- DoltHub은 Go 동시성에서 흔히 쓰는 채널을 일부러 과하게 중첩해, 채널을 채널로 보내는 장난성 예제를 만듦
- 실제로 물려받은 코드에는
chan chan struct{}가 있었고, 워커 goroutine에 새 채널을 전달하는 팬아웃(fan-out) 패턴에 쓰였지만 추론과 관리가 어려워 다시 작성됨 - 예제는 C 계열의
int****“4-star programmer” 농담을 Go의chan으로 확장해,_4chan:= make(chan chan chan chan int)를 최상위 채널로 사용함 factor = 3에서 각 채널 계층마다 producer와 consumer를 분기시키고, 마지막int값을 합산해 3의 5제곱인 243을 출력함- 실전에서는 구현·디버깅 난이도, 채널 종료 처리,
sync.WaitGroup필요성, goroutine 누수 때문에 부적합하며, 예제는 종료 로직 대신time.Sleep()에 의존함
Dolt에서 나온 채널 중첩 사례
- DoltHub은 세계 최초의 버전 관리 SQL 데이터베이스인 Dolt를 Go로 작성하고 있음
- 일반적인 Go 코드베이스처럼 channels와 goroutines를 동시 실행 구현에 사용함
- 동시성 프로그래밍은 그 자체로 어렵기 때문에, 보통은 채널과 goroutine을 단순하고 직관적인 방식으로 다룸
- 한때 다른 오픈소스 프로젝트에서 가져온 코드에는 다음처럼 채널을 보내는 채널이 있었음
var c chan chan struct{}
- 이 구조는 goroutine 사이에서 채널을 전달해 워커 goroutine의 팬아웃 패턴을 구현하는 방식이었음
- 중간 채널은 새로 생성된 채널을 실제 작업을 하는 워커에게 넘기는 중개자 역할을 함
- 동작은 했지만, 특히 goroutine 누수까지 고려하면 추론하고 다루기 어려웠음
- 해당 코드는 다시 작성됐고
chan chan struct{}는 사라짐
“4-star programmer” 농담의 Go 버전
- C와 그 파생 언어가 널리 쓰이던 시절에는 포인터를 이해하기 어려워하는 초보자를 두고 “4-star programmer”라는 농담이 있었음
- 대표 예시는
int****처럼 여러 단계의 포인터 간접 참조를 쓰는 코드임 - Go도 C에서 크게 파생됐기 때문에 포인터로 같은 식의 코드를 작성할 수 있음
*int,**int,***int,****int를 차례로 넘김- 마지막 함수에서
****i = 100을 수행하면 프로그램은i is now 100을 출력함
- Go에는 C에는 없는
chan이 있으므로, 같은 농담을 채널 간접 참조로 확장할 수 있음
4단계 채널로 5제곱 계산하기
- 최상위 채널은 다음처럼 선언됨
_4chan := make(chan chan chan chan int)
- Go 식별자는 숫자로 시작할 수 없어 예제에서는
_4chan이름을 사용함 _4chan으로 보내는 값은 3단계 채널임_3chan := make(chan chan chan int)
- 같은 방식으로 계층을 내려가다가 마지막에는 값 채널인
chan int에 도달함 - 각 간접 참조 계층에서는 상수
factor에 따라 producer를 생성함- 예시에서는
const factor = 3 sendChanChanChan은 3채널 producer를 goroutine으로 시작함
- 예시에서는
- consumer 쪽도 각 계층에서 들어온 채널을 받아 다음 단계 consumer를
factor만큼 시작함receiveChanChanChan은_4chan에서_3chan을 받고, 3채널 consumer를 시작함
마지막 계층의 값 전송과 합산
- 최하위 계층에서는 더 이상 채널이 아니라 실제
int값을 전송함 send함수는_2chan에_1chan을 보낸 뒤,factor만큼 int producer를 시작함- 각 int producer는 다시
factor만큼 goroutine을 만들어_1chan <- 1을 실행함 - consumer는 받은 정수를 전역
sum에 더함sum은atomic.Int32로 선언됨receive(c chan int)는 채널에서 값을 받아sum.Add(int32(s))를 수행함
실행 결과와 분기 수
- 전체 프로그램은
_4chan을 만들고, 송신 계층과 수신 계층을 각각 goroutine으로 시작한 뒤500 * time.Millisecond동안 대기함 - 예제 출력은 다음과 같음
3 ^ 5: 243
- 이 프로그램은 숫자의 5제곱을 최대한 분산된 방식으로 계산하는 일반화된 예제임
- 실행 가능한 예제는 Go Playground에서 볼 수 있고, 문법 강조 버전은 GitHub Gist에 있음
- 더 큰
factor를 쓰려면 실행이 끝날 수 있도록Sleep시간을 늘려야 할 수 있음 - 로그를 켜면 각 채널 생산·소비 계층의 분기 수를 확인할 수 있음
starting 3chan producer: 3회starting 2chan producer: 9회starting 3chan consumer: 9회starting 2chan consumer: 27회starting chan producer: 27회starting 1chan consumer: 81회starting int producer: 81회sending int: 243회received int: 243회
실전 코드에서 피해야 하는 이유
- 이 방식은 실제 코드에 쓰기에는 구현과 디버깅이 까다로움
- 채널을 채널로 보내면 각 채널을 언제 닫아야 하는지 판단하기 어려워짐
- 실제 사용 사례라면 채널을 닫아야 하지만, 종료 로직을 넣으려면 모든 채널 전송이 끝났는지 추적해야 함
- 종료 처리를 구현하려면 곳곳에
sync.WaitGroup을 추가해야 했고, 그러면 장난성 예제가 읽기 어려워짐 - 최종 예제는 종료 로직 대신
time.Sleep()을 사용하고, 많은 goroutine 누수를 남기는 방식으로 단순화됨