GN⁺: 4-chan Go 프로그래머
(dolthub.com)소개
- 우리는 세계 최초의 버전 관리 SQL 데이터베이스인 Dolt를 Go 언어로 작성하고 있음
- 대부분의 Go 코드베이스처럼 채널과 고루틴을 사용하여 동시 실행을 구현함
- 일반적으로 동시 프로그래밍은 어렵기 때문에 간단하고 직관적인 방법을 사용함
- 그러나 다른 오픈 소스 프로젝트에서 채널을 매우 독창적으로 사용하는 코드를 상속받았음
var c chan chan struct{}
- 이는 다른 고루틴 간에 채널을 전달하는 방식으로, 작업자 고루틴 간의 팬아웃 패턴을 구현함
- 이러한 방식은 이해하기 어렵고 고루틴 누수를 고려할 때 작업하기 어려웠음
- 결국 이 코드를 다시 작성하여
chan chan struct{}
를 제거함
왜 이런 일을 하는가
- C 언어와 그 파생 언어가 지배적이던 시절의 오래된 프로그래밍 농담이 있음
- 많은 사람들이 포인터를 이해하는 데 어려움을 겪었음
- Go도 C에서 파생된 언어이기 때문에 동일한 작업을 수행할 수 있음
func main() {
i := 1
setInt(&i)
fmt.Printf("i is now %d", i)
}
func setInt(i *int) {
setInt2(&i)
}
func setInt2(i **int) {
setInt3(&i)
}
func setInt3(i ***int) {
setInt4(&i)
}
func setInt4(i ****int) {
****i = 100
}
- 이 코드는 컴파일되어
i is now 100
을 출력함 - Go에서 채널을 사용하여 동일한 작업을 수행할 수 있음
4-chan Go 프로그래머
- 4단계의 채널 간접 참조를 사용하는 프로그램을 작성할 것임
- 최상위 채널은 4-chan으로 선언됨
_4chan := make(chan chan chan chan int)
- 이 채널에 보내는 값은 3-chan임
_3chan := make(chan chan chan int)
- 각 간접 참조 단계에서 일정한 분기 요소에 따라 생산자를 생성함
func sendChanChanChan(c chan chan chan chan int) {
for range factor {
go func() {
logrus.Debug("starting 3chan producer")
_3chan := make(chan chan chan int)
sendChanChan(c, _3chan)
}()
}
}
- 소비자도 동일하게 처리함
func receiveChanChanChan(c chan chan chan chan int) {
for _3chan := range c {
logrus.Debug("got message from 4chan")
for range factor {
logrus.Debug("starting 3chan consumer")
go receiveChanChan(_3chan)
}
}
}
- 마지막으로 실제 값을 보내는 단계에 도달함
func send(_2chan chan chan int, _1chan chan int) {
_2chan <- _1chan
for range factor {
go func() {
logrus.Debug("starting int producer")
for range factor {
go func() {
logrus.Debug("sending int")
_1chan <- 1
}()
}
}()
}
}
- 소비자는 받은 값을 합산함
var sum = &atomic.Int32{}
func receive(c chan int) {
for s := range c {
logrus.Debug("received int")
sum.Add(int32(s))
}
}
- 모든 것을 합쳐서 실행함
const factor = 3
var sum = &atomic.Int32{}
func main() {
// logrus.SetLevel(logrus.DebugLevel)
_4chan := make(chan chan chan chan int)
go sendChanChanChan(_4chan)
go receiveChanChanChan(_4chan)
time.Sleep(500 * time.Millisecond)
fmt.Printf("%d ^ 5: %d", factor, sum.Load())
}
- 이 프로그램은 숫자의 5제곱을 최대한 분산된 방식으로 계산함
논평
- 실제 코드에서 이렇게 하지 말아야 할 이유가 많음: 구현 및 디버깅의 어려움, 자존심 문제, 동료들의 비난 등
- 그러나 매우 재미있고 작동한다는 점에서 흥미로움
- 실용적인 이유 중 하나는 채널을 채널로 보내면 닫기가 매우 어려워짐
결론
- Go에서의 재미있는 동시성 패턴에 대해 질문이나 의견이 있으면 Discord에서 우리 팀과 다른 Dolt 사용자들과 대화할 수 있음
GN⁺의 정리
- 이 글은 Go 언어에서 채널을 사용한 독창적인 동시성 패턴을 다루고 있음
- 실제 코드에서 사용하기에는 비효율적이지만, 개념적으로 흥미로움
- Dolt와 같은 프로젝트에서 Go의 동시성 기능을 어떻게 활용할 수 있는지 보여줌
- 유사한 기능을 가진 프로젝트로는 PostgreSQL, MySQL 등이 있음
Hacker News 의견
-
과학자로서 전문 소프트웨어 엔지니어와 함께 일할 때, 그들이 하는 많은 일이 이해되지 않음
- 코드 한 줄이 4개의 "인터페이스 함수"를 거쳐 호출되는 것을 본 적이 있음
- 각 함수는 서로 다른 파일과 폴더에 있어 코드 읽기가 매우 피곤해짐
- 몇 단계 들어가면 실제로 계산하는 부분에 도달할지 의문이 생김
-
저조한 노력의 비실질적인 댓글을 남기고 싶음
- 첫 몇 단락의 밈이 C 프로그래머로서 웃겼음
- 언어의 이상한 변형을 보는 것을 좋아하며, Go에서 이를 보는 것이 흥미로움
-
C와 그 파생 언어가 지배하던 시절의 오래된 프로그래밍 농담이 여전히 유효함
-
Buena Vista Social Club의 클래식 음악이 떠오름
-
"chan chan Value" 또는 "chan struct{resp chan Value}" 패턴을 특정 상황에서 사용한 적이 있음
- 메시지 버스를 대신 사용할 수 있었지만, 메시지 버스를 처리해야 하는 상황이 됨
-
채널의 채널은 일반적인 패턴이며, 보통 구조체 타입의 필드가 채널인 형태로 나타남
- 요청을 보내고, 작업자가 작업을 완료한 후 결과를 응답 채널에 넣는 방식
-
type request struct { params, reply chan response }
와 같은 형태 - 두 개의 채널이 유용하며, 세 개 이상의 채널은 본 적이 없음
-
동적 디스패치 메커니즘을 구현하기 위해 채널을 사용하는 반대 의견 블로그
- Limbo 언어에서 사용되며, Go와 동일한 개념
- 블로그 링크
-
Joe Armstrong의 "My favorite Erlang Program"을 떠올리게 함
-
링크를 클릭했을 때 다른 것을 기대했음
- Go 프로그래머가 아니어서 농담을 바로 알아차리지 못함
-
LabVIEW 코드에서 비동기 응답 데이터를 받기 위해 유사한 방식 사용
- 응답을 큐에 덤프하는 대신, 콜백 이벤트 채널을 포함한 메시지를 전달
- 메모리 낭비가 있지만, 단일 사용 후 응답 시 닫히므로 효율적임