고랭 채널의 내부 구조와 동작 방식에 대해 살펴보자!
채널은 send한 순서대로 데이터를 receive하게 된다.
thread-safe란 멀티 thread 프로그래밍에서 여러 쓰레드로부터 변수에 동시에 접근이 이뤄져도 프로그램의 실행에 문제가 없음을 뜻한다.
goroutine으로 치환하면, 여러 고루틴이 하나의 채널에 동시에 접근해도 프로그램 실행에 문제가 없다는 얘기다.
size := 10
ch := make(chan int, size)
// goroutine1
go func() {
for i := 0; i < size; i++ {
ch <- i
}
}()
// goroutine2
go func() {
for {
select {
case value := <-ch:
}
}
}()
1번 goroutine에서 send한 데이터를 2번 goroutine에서 receive할 수 있다.
채널을 사용하다 보면 어떤 시점에 goroutine이 block되기도, unblock되기도 한다.
ch := make(chan int, 3)
go func() {
ch <- 1
ch <- 2
ch <- 3
ch <- 4 // block
// do something
}
이미 채널 버퍼가 꽉 찬 상태에서 데이터를 receive하려고 하면 위 goroutine은 block되어서 그 후의 dosomething을 수행할 수가 없다.
ch := make(chan int, 3)
go func() {
ch <- 1
ch <- 2
ch <- 3
ch <- 4 // 2) unblock
// 3) do something
}
go func() {
<-ch // 1) 이게 실행되면
}
위 고루틴이 block된 시점에, 또 다른 고루틴이 데이터를 receive하면 채널에 자리가 생기면서,
block이 풀리고 (unblock) ch <- 4
, do something
이 동작하게 된다.
내부 구조와 동작 방식을 통해 어떻게 위 4가지 특징이 구현됐는지 알아보자~
ch := make(chan int, 3)
make 명령어를 통해 channel을 생성하면 어떤 일이 생길까?
위와 같은 명령어를 입력하면 내부적으로 hchan struct가 만들어진다.
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
이 hchan은 고랭 내부에 정의된 것으로 여기에 위치한다.
여러가지 필드와 변수가 있지만 기본적인 것은 아래와 같다.
buf unsafe.Pointer
데이터가 하나도 없을 때, sendx: 0
내가 이 상태에서 데이터를 하나 send 한다면, 0번 index에 데이터가 들어간다.
데이터가 하나 추가됐을 때, sendx: 1
이 상태에서 데이터를 하나 send하려고 한다면, 1번 index에 데이터가 들어간다.
데이터가 한번도 receive한 적이 없을 때, recvx: 0
이 상태에서 데이터를 하나 receive하려면 0번 index에 있는 데이터를 receive하게 될 것이다.
데이터를 하나 receive한다면, recvx: 1
hstruct는 heap에 위치하고, ch은 hstruct에 대한 포인터이다.
그래서 "서로 다른 goroutine에서 send/receive할 수 있다."가 가능하다.
💡 FIFO: sendx, recvx가 원형큐 형태로 동작하여 구현
💡 서로 다른 goroutine에서 send/receive할 수 있다: hchan이 heap에 위치하기 때문
ch <- task
이 2가지 특징을 알 수 있다.
t := <-ch
mutex lock
data copy
t 변수에 data를 copy한다.
mutex lock을 풀어준다.
💡 goroutine-safe: mutex가 있기 에 가능
채널 자체는 goroutine-safe하다. 하지만 채널에 데이터를 send/receive하는 변수는 goroutine-safe를 보장할 수 없다!
var globalNumber int
func Send() {
globalNumber++
ch <- globalNumber
}
채널 내부에서는 goroutine-safe를 보장하지만, "globalNumber"의 goroutine-safe는 보장되지 않는다. receive도 마찬가지다.
block, unblock이 어떻게 구현되는지에 앞서 고루틴에 대해 간략한 이해가 필요하다.
기본적인 구조는 위와 같다.
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
ch <- 4 // block!
이 과정을 알기 위해서 기본적으로 sudog라는 객체를 알아야 한다.
type sudog struct {
g *g
elem unsafe.Pointer // data element (may point to stack)
// and more
}
sudog도 go 내부에 존재하는 객체이다.
여러가지 필드가 있지만 기본적으로 g와 elem을 알아야 한다.
즉 고루틴과 해당 고루틴이 쓰는 변수 정보를 가지고 있는 구조체가 sudog이다.
이런 식으로 대기하고 있는 고루틴 정보를 저장하고 있다.
type hchan struct {
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// ... and more
}
이쯤에서 다시보는 hchan. hchan 안에는 recvq와 sendq 필드가 존재하고 요놈들의 데이터 타입은 waitq이다.
type waitq struct {
first *sudog
last *sudog
}
waitq는 이전 sudog, 다음 sudog를 저장하고 있어 즉 recvq, sendq는 연결리스트로 sudog 정보를 저장한다.
먼저 block되는 상황이 발생한다.
ch := make(chan int, 3)
go func() {
ch <- 1
ch <- 2
ch <- 3
ch <- 4 // block
// do something
}
block되는 상황이 발생하면 해당 고루틴의 sudog 구조체를 생성한다.
send를 대기중인 sudog 목록인 sendq (linkedlist)에 추가한다.
gopark이라는 함수를 사용하여, processor가 해당 고루틴을 block 처리하게 한다.
ch := make(chan int, 3)
go func() {
ch <- 1
ch <- 2
ch <- 3
ch <- 4 // 2) unblock
// 3) do something
}
go func() {
t := <-ch // 1) 이게 실행되면
}
recvx (receive할 차례)에 해당하는 데이터를 복사한다.
sudog에 있는 elem (send하려던 값)을 buf에 copy해서 channel에 send한다.
goready라는 함수를 통해 대기중이었던 고루틴을 실행할 수 있게 한다.
ch := make(chan int, 3)
// g1
go func() {
ch <- 1 // 2) send
}
// g2
go func() {
t := <-ch // 1) block
// dosomething 3) unblock
}
위와 같이, 비어있던 채널에 대해 block을 하다가 데이터가 send되면서 receive하는 과정은 어떻게 될까?
위 설명대로라면 다음과 같이 실행되겠지만 조금 비효율적이다.
즉 g1 stack에 있던 데이터가 바로 g2 stack으로 복사된다!
buffer가 없는 고루틴도 마찬가지 방식으로 돌아간다.
💡 block, unblock
1. hchan 내부에 send 대기중인 고루틴 목록/receive 대기중인 고루틴 목록을 저장하고 있는다.
2. sudog라는 구조체로 고루틴 정보와 channel에 보낼 데이터 (stack에 대한 포인터) 정보를 가지고 있는다.
3. 고루틴은 상태가 변경됨에 따라 block, unblock되며, 이는 모두 고루틴 런타임 레벨에서 동작한다.
혹시 고랭하게 된 계기가 있나요????