Backend with Golang

[Concurrency-2] Chapter 3 channel 전까지

아직개구리 2023. 6. 12. 17:10

 Go는 model of concurrency 로 fork-join model를 따른다. 

  • fork라는 단어는 parent와 concurrently 하게 동작하기 위해서 child branch를 split off 할 수 있음을 의미한다. 
  • join이라는 것은 미래의 한 시점에 parent로 다시 branch가 join된다는 것을 의미한다. 
sayHello := func {
	fmt.Println("hello")
}
go sayHello()
//continue doing other things

 

main function 의 나머지 부분들은 빠르게 실행될 것이고, sayHello 함수가 실행될지 안될지는 undetermined이다. 

join point를 만드는 것이 중요하다. 그래서 race condition일 뿐인 time.Sleep을 사용하는 것은 답이 안된다. 

이것은 exiting 하기 전에 실행될 확률을 높이는 것 뿐이지, 그것을 보장하는 방법이 아닌 것이다. 

Join Point는 program의 correctness 를 보장하고 race condition을 제거해준다. 

-> 이것을 막아주는  방법 중 하나가 sync.WaitGroup을 사용하는 방법이다. wg.Wait()가 join point가 되어준다. 

 

 Closure 는 그 body 밖에서 variable을 참조하는 함수를 의미하는데, access 할수도 있고 assign할수도 있다. closure를 사용할 때 for loop 안에서 go function을 하면, for loop이 먼저 끝나서, go routine이 실제로 실행될때 for loop의 가장 마지막 원소만 참조할 수 있는 문제가 있는데, 이는 각 function 에 argument로 copy 된 value를 전달함으로써 해결할 수 있다. 

 

 Go가 어떻게 memory를 관리하는 지에 대한 흥미로운 노트가 있다. Go runtime은 어떤 variable에 대해서 참조한 것이 여전히 들고  있다는 것을 알정도로 observant하고, 그 memory를 heap으로 전달해서 goroutine이 계속 접근할 수 있도록 한다. 

그리고 Go compiler의 경우 같은 address space를 사용하는 경우에는 memory에 variable을 고정시키고 freed memory에 접근하지 않도록 잘 핸들링한다. (하지만 same address space가 아닌 경우에는 synchronization에 대해서 걱정해야만 한다.) 

 

garbage collector는 goroutine 이 어떻게 버려졌던 그것을 수집하는 것에 아무것도 하지 않는다. 

goroutine은 process가 exit될때까지 계속 있는 것이다. 이 점을 사용해서 goroutine의 size를 측정할 수 있고, 측정해봤을 때 몇kb 단위가 된다는 것을 알 수 있다. 

 

여기서 의심! 여러개의 concurrent process들을 hosting한다면, context switching 에 많은 시간을 들일거라 예상할 수 있다. OS레벨에서는 꽤 비용이 비싸다고 할 수 있다. OS thread는 register value, lookup tables나 memory maps등을 저장해야하고 그래야 성공적으로 되돌아올 수가 있다.  하지만 OS level이 아닌 software level에서는 상대적으로 비용이 굉장히 싸다.

 

Sync Package

Waitgroup은 concurrent operation의 result를 신경쓰지 않아도 되거나, result를 콜렉팅 하는 다른 방법이 있을 때 쓰면 좋다. 

그렇지 않으면, select구문이나, channel을 사용하는 것이 좋다. 

Add 와 goroutine을 최대한 가깝게 하는 것은 관례에 가까운데 이는 goroutine을 한번에 트래킹 하기 좋기 때문이다. 

 

Mutex

Mutex란 mutual exclusion을 의미하고, critical section을 보호하는 방법으로 쓰인다. 

defer statement와 함께 Unlock 하는 것이 같이 쓰인다. 

 panic이라도 항상 unlock이 불리는 것을 보장하는데, 이걸 실패하면 deadlock을 야기할 수 있다. 

 

Critical section은 이름 자체에서도 알 수 있듯이 program의 bottleneck을 반영하는데, 이는 critical section을 들어가고 나오는 것이 어떤 면에서 expensive하고 그래서 사람들이  critical section에서 소비되는 시간을 최소화하려고 애쓰곤 한다. 

 

RWMutex를 사용하면, Read lock같은 경우는 서로를 막지 않고, 읽기 시도중에 값이 바뀌면 안되니 write lock은 막는다. 아무도 writer lock 을 가지고 있지 않으면, 임의의 수의 reader가 reader lock을 가지고 있을 수 있다.  critical section에서 어떤 일을 하는가에 따라 달라지겠지만, 논리적으로 말이된다면 RWMutex를 사용하는 것이 타당하다 .

 

Cond

event 는 임의의 시그널인데, 두개나 그 이상의 goroutine 사이에서 보내지는 시그널인데, 이것은 일어났다는 사실 이외에 어떤 정보를 보내지 않는다. Cond타입은 이 이벤트가 일어나기를 기다리는 어떤 집결지라고 보면 된다. 

뮤텍스를 사용해서 조건변수를 생성하게 되면, 

 

cond method들

  • Wait(): 고루틴 실행을 멈추고 대기
  • Wait에 의해 가로막혀진 고루틴들에게 컨디션이 trigger 되었다고 알려주는 함수들이다. 
    • Signal(): 대기하고 있는 고루틴 하나만 깨움. signal 은 가장 오래 기다린 고루틴을 찾고 그것에게 알려준다.
    • Broadcast(): 대기하고 있는 모든 고루틴을 깨움. signal을 모든 고루틴에게 보낸다. 여러개의 고루틴과 커뮤니케이션 할 수 있다는 점에서 흥미롭게 비춰진다. 

mutex를 사용해서 cond.wait를 보호하고, cond.wait을 하고 대기 상태에 있던 고루틴들을 singal을 사용해서 하나씩 깨우거나, broadcast를 사용해서 모든 고루틴을 깨우거나 이다. channel을 사용해서 cond의 signal은 만들 수 있지만 broadcast는 어렵다. cond type은 channel을 사용하는 것보다 훨씬 효과적이다. 

 

Pool

풀 자체는 이제 사용할 것들에 대해서 가능한 고정된 숫자를 가진 pool을 만들어 놓는데 쓰이는데, 이는 보통 creation이 비싼 예를 들면 database connections 같은 것에 대해서 creation을 제한하기 위해서 쓰이는데, 이는 고정된 숫자만 생성되게 하는 반면에 indeterminate 한 숫자의 operation이 요청이 들어오게 된다.  

 

Pool 자체는 pre-allocated objects들을 데펴서 최대한 빠르게 실행될 수 있게 만드는데에 많이 쓰이곤 하는데, host machine의 메모리를 보호하는 역할도 한다. 왜햐냐면 object creation의 숫자를 제한하기 때문이다.