Backend with Golang

[Concurrency-4] Concurrency Patterns in Go

아직개구리 2023. 6. 17. 22:02
  1. Synchronization 이 가능한데 confinement 를 추구하는 이유가 뭘까? improved performance와 reduced cognitive load on developers를 이유로 들고 있다. Synchronization 이란 여기서 수행되는 시간들을 조정해서 multiple thread나 process 에서 하나의 데이터가 일치되는 것을 의미할 텐데, 이것은 비용이 들고, 이것을 쓰지 않고 피할 수 있다면 critical section 이 없어지고, synchronizing 하는데에 드는 비용을 쓰지 않아도 될 것이다. 그리고 synchronization을 적용해서 얻을 수 있는 여러 문제점들을 피할 수 있게 된다. 결과적으로 lexical scope 안에서 synchronous code를 작성하면 되기 때문에, lexically confined된 concurrent code일 때 이해하기 쉬워지는 것이다. 
  2.  for-select loop 의 경우 
    1. Sending iteration variables out on a channel 
    2. Looping infinitely waiting to be stopped: 이것의 경우 스타일 선호도의 차이로 default 아래에 진행되어야 하는 work를 넣던지, 아니면 select 문 바깥에 넣던지 선택할 수 있다. 
  3. Preventing Goroutine Leaks
    1. Go runtime 은 어떤 갯수더라도 os threads에 goroutine을 multiplexing 하는 것을 책임지기 때문에, 우리는 이런 추상화 레벨에 대해서 고민할 필요가 없다. 하지만 resource들에 대한 비용은 지불해야하고, 고루틴들은 runtime에 의해서 garbage collected되지 않기 때문에, 아무리 적게 memory를 쓴다고 해도, 남겨놓지 않고 싶을 것이다. 어떻게 cleaned up 할 것인가? 
    2. done 채널을 사용해서 clean 할 수 있다. 여기서 하나의 고루틴이 다른 하나의 고루틴을 생성하는 데 책임이 있다면, stop을 보장하는 것에도 책임이 있다. 라는 convention을 규정할 수 있다. 

Or Channel 

  •  or function 을 정의해서 사용할 수 있는데, 이는 channel을 slice로 받고, single channel을 return 하는 함수이다. or channel은 하나 이상의 done channel들이 하나의 done 채널로 합쳐넣어서 하나만 close되더라도 모두 close 될 수 있도록 하는 것을 위해 쓰인다. 물론 select문을 사용해서 coupling 할 수 있지만, 몇개의 done channel이 runtime에 실행되는지 모르는 경우도 때때로 있다. 이때 or channel패턴을 사용해서 한줄로 해결 할 수 있다. 
var or func(channels ...<-chan interface{}) <-chan interface{}

** 생성된 고루틴의 갯수를 걱정하는 것은 섣부른 최적화와 같다고 한다. go의 강점은 빠르게 고루틴을 create, schedule, run 할 수 있는 것이기 때문이다. 

** 그리고 만약에 runtime에 몇개의 done 채널을 실행하고 있는 지 모른다면, or channel 외에 이걸 combine하는 방법은 없다. 

 

Error Handling 

  • Multiple concurrent processes를 사용할 때 에러 핸들링을 어떻게 하면 좋은지에 대해서 살펴본다. 에러 핸들링에서 가장 근본적인 문제는 Who should be responsible for handling the error? 이다. concurrent process 는 parent 나 sibling 하고 독립적으로 운영되기 때문에 error를 올바르게 처리하는 것이 어려워 질 수 있다.
  • potential result 와 potential error를 coupled 한 예시: 일어날 수 있는 결과들을 모두 포함한 complete set을 제공하기 때문에, main goroutine에서도 error가 일어났을 때 어떤 행동을 취해야하는지 선택할 수 있다. producer goroutine에서 goroutine들이 spawned되기 때문에 실행되고 있는 프로그램에 대해서 더 많은 context를 가지고 있기 때문에 에러에 대해서 더 intelligent한 결정을 내릴 수 있고, 이 점 때문에 producer 고루틴이 이 정보를 얻어야하는 것이 desirable한 것이다. 
type Result struct {
    Error error
    Response *http.Response
}

 

  • 여기서 가장 중요한 takeaway는 error가 생길 수 있는 고루틴이라면 무조건 result type과 tightly coupled되어서 return 되어야 한다는 점이다.

 

Pipelines 

하나의 logical change를 만드는 데 여러개의 영향범위를 건드린 적이 있었는가? 이는 poor abstraction의 결과일 가능성이 있다. 

pipeline이란 abstraction을 형성하는 데 사용할 수 있는 tool이다. 특히 만약에 프로그램이 stream을 처리해야하거나 data batch를 처리해야한다면 더더욱 powerful tool 이 될 것이다. pipeline이란 data를 받아들이고, operation을 실행하고, data 를 도로 내놓는 일련의 과정들을 의미한다. 그래서 이런 operation들을 stage of the pipeline 이라고 한다. 

pipeline의 특성을 가지도록 함수를 construct했기 때문에 pipeline을 형성하도록 combine할 수 있게 되는데, 여기서 pipeline stage의 특성들은 무엇일까? 

  • consumes and returne the same type
  • stage는 reified 되어있어야 한다. Go의 function들이 reified라고 말하는 것은 variable들이 function signature 의 type을 가지도록 정의할 수 있다는 것을 의미하는데, 이는 동시에 function을 전달할 수 있다는 것을 의미한다. 

이것을 만족시키면 되고 이때 이것들이 higher order functions나 monad를 떠올리게 할 수 있는데, 이는 functional programming에서 나오는 개념들이고, useful하니 찾아보면 좋을듯 

 

  • Stream processing : receives and emits one element at a time. 
  • Batch processing : operate chunks of data all at once 

두개의 pros and cons가 각각 있는데, batch processing에서 기존 slice형태의 value가 변경되지 않으면, memory footprint 가 size of slice 의 두배가 될 것인데, stream processing에서는 pipeline의 input 크기만큼이 된다.  stream의 경우에 pipeline 작성을 for loop 안에 해야하고, range 가 pipeline 을 feeding하는 작업을 해야한다. 이것은 feed 하는 것을 재사용하는 데 제한이 걸리는 것 뿐만아니라 scale하는 데 제한이 걸리게 된다. 

 

pipeline의 장점이 individual stage를 동시적으로 실행할 수 있다는 것이었다. Go 에서 pipeline을 constructing 하는 데 사용 되는 best practices를 알아보자. 먼저 channel primitive를 사용하는 것으로 시작~ 

Best Practices for Constructing Pipelines 

channel 은 pipeline으로 사용되기 위한 조건들을 다 갖췄다! receive, emit values, safely be used concurrently, ranged over될 수 있고, reified by the language! 

 

 

 

 

 

  • 비선점형 스케쥴링(Non-Preemptive Scheduling): 어떤 프로세스가 cpu를 할당 받으면 프로세스가 종료되거나 입출력 요구가 발생해서 자발적으로 중지될 때까지 계속 실행되도록 보장한다. 하나의 프로세스가 끝나지 않으면 다른 프로세스는 cpu를 사용할 수 없다 라는것을 의미한다. 
  • 채널이 닫힌 후에는 데이터를 send 할 수 없지만 채널이 닫힌 후에는 계속 receive가 가능하다.