Goroutine에서 채널로: Go의 CSP 모델 이해
Lukas Schneider
DevOps Engineer · Leapcell

서문
Go의 CSP 동시성 모델 구현은 두 가지 주요 구성 요소로 이루어져 있습니다. 하나는 Goroutine이고, 다른 하나는 채널입니다. 이 글에서는 기본적인 사용법과 주의해야 할 점을 소개합니다.
Goroutine
Goroutine은 Go 애플리케이션의 기본 실행 단위입니다. 경량화된 사용자 수준 스레드이며, 동시성 구현의 기반은 코루틴입니다. 잘 알려진 바와 같이 코루틴은 사용자 모드에서 실행되는 사용자 스레드입니다. 따라서 Goroutine도 Go 런타임에 의해 스케줄링됩니다.
기본 사용법
문법:
go
+ 함수/메서드
go
키워드 뒤에 함수/메서드를 사용하여 Goroutine을 생성할 수 있습니다.
예제 코드:
import ( "fmt" "time" ) func printGo() { fmt.Println("Named function") } type G struct { } func (g G) g() { fmt.Println("Method") } func main() { // 명명된 함수로부터 goroutine 생성 go printGo() // 메서드로부터 goroutine 생성 g := G{} go g.g() // 익명 함수로부터 goroutine 생성 go func() { fmt.Println("Anonymous function") }() // 클로저로부터 goroutine 생성 i := 0 go func() { i++ fmt.Println("Closure") }() time.Sleep(time.Second) // 생성된 Goroutine이 실행될 기회를 갖기 전에 메인 Goroutine이 종료되는 것을 방지하기 위해 1초 동안 대기합니다. }
실행 결과:
Named function
Method
Anonymous function
여러 Goroutine이 존재할 때 실행 순서는 고정되어 있지 않습니다. 따라서 출력 결과는 매번 다를 수 있습니다.
코드에서 볼 수 있듯이 go
키워드를 사용하여 명명된 함수나 메서드뿐만 아니라 익명 함수나 클로저를 기반으로 Goroutine을 생성할 수 있습니다.
그렇다면 Goroutine은 어떻게 종료될까요? 일반적으로 Goroutine의 함수가 실행을 마치거나 반환되면 종료됩니다. Goroutine의 함수 또는 메서드에 반환 값이 있는 경우 Goroutine이 종료될 때 무시됩니다.
채널
채널은 Go의 동시성 모델에서 중요한 역할을 합니다. Goroutine 간의 통신 및 Goroutine 간의 동기화에 사용할 수 있습니다.
채널의 기본 연산
채널은 복합 데이터 유형이며, 선언할 때 저장할 요소의 유형을 지정해야 합니다.
선언 구문:
var ch chan string
위 코드는 요소 유형이 string
인 채널을 선언합니다. 즉, string
값만 저장할 수 있습니다. 채널은 참조 유형이며 데이터를 쓰기 전에 초기화해야 합니다. make
를 사용하여 초기화됩니다.
import ( "fmt" ) func main() { var ch chan string ch = make(chan string, 1) // 채널 주소 출력 fmt.Println(ch) // "Go"를 ch에 보냅니다. ch <- "Go" // ch에서 데이터를 받습니다. s := <-ch fmt.Println(s) // Go }
ch <- xxx
를 사용하여 채널 변수 ch
에 데이터를 보내고 x := <-ch
를 사용하여 데이터를 받을 수 있습니다.
버퍼링된 채널 vs. 버퍼링되지 않은 채널
채널을 초기화할 때 용량을 지정하지 않으면 버퍼링되지 않은 채널이 생성됩니다.
ch := make(chan string)
버퍼링되지 않은 채널에서 송신 및 수신 작업은 동기식입니다. 송신 작업을 실행한 후 해당 Goroutine은 다른 Goroutine이 수신 작업을 수행할 때까지 차단되고 그 반대의 경우도 마찬가지입니다. 송신 및 수신 작업이 동일한 Goroutine에 배치되면 어떻게 될까요? 다음 코드를 살펴보겠습니다.
import ( "fmt" ) func main() { ch := make(chan int) // 데이터 전송 ch <- 1 // fatal error: all goroutines are asleep - deadlock! // 데이터 수신 n := <-ch fmt.Println(n) }
프로그램이 실행되면 ch <-
에서 모든 Goroutine이 잠들어 있다는 fatal error가 발생합니다. 즉, 교착 상태가 발생했습니다. 이를 피하려면 송신 및 수신 작업을 서로 다른 Goroutine에 배치해야 합니다.
import ( "fmt" ) func main() { ch := make(chan int) go func() { // 데이터 전송 ch <- 1 }() // 데이터 수신 n := <-ch fmt.Println(n) // 1 }
위의 예에서 버퍼링되지 않은 채널의 경우 송신 및 수신 작업은 두 개의 다른 Goroutine에서 수행해야 합니다. 그렇지 않으면 교착 상태가 발생합니다.
용량을 지정하면 버퍼링된 채널이 생성됩니다.
ch := make(chan string, 5)
버퍼링된 채널은 버퍼링되지 않은 채널과 다릅니다. 송신 작업을 수행할 때 채널의 버퍼가 가득 차 있지 않으면 Goroutine이 일시 중단되지 않습니다. 버퍼가 가득 찼을 때만 채널로 보내면 Goroutine이 일시 중단됩니다. 예제 코드:
func main() { ch := make(chan int, 1) // 데이터 전송 ch <- 1 ch <- 2 // fatal error: all goroutines are asleep - deadlock! }
송신 전용 및 수신 전용 채널 선언
송신 및 수신이 모두 가능한 채널
ch := make(chan int, 1)
위의 코드를 사용하면 송신 및 수신 작업을 모두 수행할 수 있는 채널 변수를 얻을 수 있습니다.
수신 전용 채널
ch := make(<-chan int, 1)
위의 코드를 사용하면 수신 작업만 수행할 수 있는 채널 변수를 얻을 수 있습니다.
송신 전용 채널
ch := make(chan<- int, 1)
위의 코드를 사용하면 송신 작업만 수행할 수 있는 채널 변수를 얻을 수 있습니다.
일반적으로 송신 전용 및 수신 전용 채널 유형은 함수 매개변수 유형 또는 반환 값으로 사용됩니다.
func send(ch chan<- int) { ch <- 1 } func recv(ch <-chan int) { <-ch }
채널 닫기
내장 함수 close(c chan<- Type)
를 사용하여 채널을 닫을 수 있습니다.
보내는 쪽에서 채널 닫기 채널이 닫히면 더 이상 채널에 보내기 작업을 수행할 수 없습니다. 그렇지 않으면 채널이 이미 닫혔음을 나타내는 패닉이 발생합니다.
func main() { ch := make(chan int, 5) ch <- 1 close(ch) ch <- 2 // panic: send on closed channel }
채널이 닫힌 후에도 여전히 수신 작업을 수행할 수 있습니다. 채널에 버퍼가 있으면 버퍼링된 데이터가 먼저 읽혀집니다. 버퍼가 비어 있으면 검색된 값은 채널 요소 유형의 zero 값입니다.
import "fmt" func main() { ch := make(chan int, 5) ch <- 1 close(ch) fmt.Println(<-ch) // 1 n, ok := <-ch fmt.Println(n) // 0 fmt.Println(ok) // false }
for-range
로 채널을 탐색할 때 반복 중에 채널이 닫히면 for-range
루프가 종료됩니다.
요약
이 글에서는 먼저 Goroutine을 생성하는 방법과 종료되는 조건에 대해 소개했습니다.
그런 다음 버퍼링된 채널과 버퍼링되지 않은 채널 모두 채널 변수를 만드는 방법에 대해 설명했습니다. 버퍼링되지 않은 채널의 경우 보내기 및 받기 작업은 두 개의 다른 Goroutine에서 실행해야 합니다. 그렇지 않으면 오류가 발생합니다.
다음으로 송신 전용 및 수신 전용 채널 유형을 정의하는 방법에 대해 설명했습니다. 일반적으로 이러한 유형은 함수 매개변수 유형 또는 반환 값으로 사용됩니다.
마지막으로 채널을 닫는 방법과 닫은 후의 몇 가지 예방 조치에 대해 다루었습니다.
Go 프로젝트 호스팅을 위한 최고의 선택, Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하세요.
무료로 무제한 프로젝트 배포
- 사용량에 대해서만 지불하세요. 요청도 없고 요금도 없습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼만 지불하세요.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
손쉬운 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장됩니다.
- 운영 오버헤드가 없어 구축에만 집중할 수 있습니다.
문서에서 자세히 알아보세요!
X에서 팔로우하세요: @LeapcellHQ