Backend with Golang

Effective Go - Part 4: Embedding & Concurrency

아직개구리 2023. 8. 16. 23:29

Embedding

Go does not provide the typical, type-driven notion of subclassing, but it does have the ability to “borrow” pieces of an implementation by embedding types within a struct or interface. subclassing이란 ? 

  • 오직 인터페이스 만이 interface안에 embedded될 수 있다.
  • struct에서도 비슷한데, interface 보다는 광범위한 영향(implication)을 끼친다. field 이름 없이 type을 listing 할 수 있고, field이름과 함께 쓰여질 수 도 있다. 하지만 이름이 있을 경우 method를 전달해주기 위해서는 그 field이름을 쓰고 나서 method를 호출 할 수 있다. 이렇게 하는 게 번거로워 피하기 위해서는 직접 embedding하면 되는데, 그렇게 하면 embed된 타입의 메서드는 자동으로 따라온다.
  • embedding 과 subclassing이 다른 점은? 임베드 했을 때 method는 outer type의 method가 되지만, method를 부를 때 receiver는 그 안의 type이 전달되어서 forwarding 하는 것과 같은 효과를 가진다.
  • 임베드된 필드를 직접 언급해야 할 경우가 생기면, readWriter struct 의 Read method처럼, 패키지를 없앤 타입명이 필드의 이름으로 사용된다. Job타입의 변수인 job.Logger라고 쓰면 된다. Logger의 메서드를 개선하길 원할 때 유용하다.
  • embedding은 name conflict문제를 가져오는데 해결하는 규칙은 간단하다.
    • 첫번째로 field나 method X 가 있으면 다른 더 깊이 nested되어있는 X는 가려진다.
    • 두번째로, 똑같은 이름이 같은 레벨로 중첩되어 나타나면 보통은 에러가 생긴다. 하지만 type definition바깥에서 프로그램에서 never mentioned 된다면, 중복되어도 된다.

Concurrency

  • channel을 통해서 value가 전달되기 때문에 다른 thread에서 공유될 수 없다. 오직 하나의 고루틴이 value에 접근하기 때문에 데이터 경쟁은 구현 설계상 발생할 수 없다. 이런 것을 권장하기 때문에, communicating을 통해서 share memroy를 한다는 슬로건이 붙었다.

Goroutine

경량 쓰레드. lightweight, stack space를 할당하는 것보다 약간 더 비싸다. 처음에 stack은 작게 시작해서, 필요해지면 allocating & freeing heap storage를 통해 사이즈가 커진다.

  • 고루틴은 여러개의 os thread로 multiplex되는데, 예를 들어 하나가 IO 를 기다리고 있다면 다른 것이 실행된다. 쓰레드 생성이나 관리에 대해서 몰라도 되는 장점이 있다.
  • function literal은 closure이다. 이 구현은 함수안에서 참조된 variable들이 active 할때까지 살아있음을 보장한다.

Channels

함수가 완료되었다는 시그널이 없기 떄문에 실용적이지 않은데, 이떄 channel이 필요하게 된다.

  • map과 마찬가지로 make로 할당되고, 값은 실제 데이터 구조에 대한 참조로 동작한다.
  • 수신부는 수신할 데이터가 있을때까지 항상 block된다. unbuffered channel인 경우 송식부는 수신부가 값을 받을 때까지 블락되고, buffered channel이면, 값이 버퍼에 복사 될때까지 블락된다. 버퍼가 꽉 차면 수신부가 값을 획득할 때까지 대기중이라는 것을 의미한다.
  • 서버를 작성하는 일반적인 문제로 돌아가면, 리소스를 잘 관리하는 방법은 요청 채널을 읽는 모든 handle 고루틴을 고정된 수에서 시작하는 것이다. 고루틴의 수를 제한하는 것은 처리할 동시 호출 수를 제한하는 것이다. 결과적으로 아래와 같이 handle goroutine을 고정된 수에서 시작하게 되면 resource를 매니지하는 다른 방법이 될 수 있다. serve channel은 exit하라고 말하는 channel도 받아서 그것에서 값을 받기 전까지는 block된다.
func handle(queue chan *Request) {
    for r := range queue {
        process(r)
    }
}

func Serve(clientRequests chan *Request, quit chan bool) {
    // Start handlers
    for i := 0; i < MaxOutstanding; i++ {
        go handle(clientRequests)
    }
    <-quit  // Wait to be told to exit.
}

Channels of Channels

  • Go 에서는 channel도 다른 것처럼 allocated & passed될 수 있는 first class value 라는 것이다. 여기서 allocated란? first class value란?
  • safe, parallel demultiplexing하는 과정에 쓰인다
  • client는 함수와 인자, 그리고 요청 객체 안에서 응답을 수신하는 채널을 제공하는 형태이다.
type Request struct {
    args        []int
    f           func([]int) int
    resultChan  chan int
}

// 이때 서버측에서 할 일은 위의 코드에서 handler만 변경하면 된다. 
func hanlde(queue chan *Request) {
	for req := range queue {
		req.resultChan <- req.f(req.args) 	
	}
}

 

Parallelization

  • 벡터 아이템에 대한 실행비용이 비싼 연산이 있다고 했을 때 부분을 나눠서 CPU당 하나씩 실행시키도록 할 수 있고, 마지막에 for문에서 모든 태스크가 끝날때까지 channel에서 값을 수신하면 된다.
  • Go는 동시성 특징을 가지고 있기 떄문에 일부 문제를 병렬계산으로 쉽게 구조화 할 수 있지만, 모든 병렬화 문제가 go에 맞진 않는다.
  • https://go.dev/blog/waza-talk

Leaky buffer ?

어디에 쓰이나 ? 

  • 100개가 넘어가게 되면 새롭게 할당되고, 버려지는 게 많아질테
  • 재사용성이 얼마나 되려나?

 

Mastering Go 3rd edition 동시성 부연 설명 

Unbuffered channel과 Buffered Channel 

  • Unbuffered channel : 값을 전달하면 값을 누가 receive할때까지 block 된다. 
  • Buffered Channel: sender는 buffer가 다 찼을 때만 block, receive할때는 buffer가 비어있을 때 블락된다.
    • Buffered chan 의 경우, 더 많은 요청을 처리할 수 있게 수행할 작업을 queue에 빠르게 넣을 수 있다.
    • 처리량을 제한하고자 버퍼 채널을 semaphore처럼 사용할 수 있다. 
      • Semaphore 자료: https://copycode.tistory.com/62 
      •  Mutex는 lock 을 통해서 하나의 쓰레드 만이 동일한 시점에 mutex를 얻어 critical section에 들어올 수 있고, 이 쓰레드 만이 critical section에서 나갈 때 mutex를 unlock할 수 있다. semaphore는 락을 건 쓰레드가 아니어도 signal을 보내 락을 해제할 수 있다.

Channel 관련

  • Range 키워드와 함께 채널을 사용하면, range loop은 채널이 closed되거나 break 키워드를 사용할 때만 빠져나올 수 있다. 
  • closed channel의 값은 읽을 수 있다. zero 값을 리턴한다. closed channel 에 값을 쓰려고 하면 panic에러가 난다. 
  • close 하는 건 read only 변수로 받으면 close 함수 사용 불가능하다. 
  • nil채널에 읽고 쓰기, close 안됨. 

 

이해 안가는 부분

https://go.dev/doc/effective_go#concurrency

 

Effective Go - The Go Programming Language

Documentation Effective Go Effective Go Introduction Go is a new language. Although it borrows ideas from existing languages, it has unusual properties that make effective Go programs different in character from programs written in its relatives. A straigh

go.dev

One way to think about this model is to consider a typical single-threaded program running on one CPU. It has no need for synchronization primitives. Now run another such instance; it too needs no synchronization. Now let those two communicate; if the communication is the synchronizer, there's still no need for other synchronization. Unix pipelines, for example, fit this model perfectly. Although Go's approach to concurrency originates in Hoare's Communicating Sequential Processes (CSP), it can also be seen as a type-safe generalization of Unix pipes.

'Backend with Golang' 카테고리의 다른 글

[Concurrency-5] Fan-Out, Fan-In  (0) 2023.09.12
Rate Limiting  (1) 2023.09.06
Effective Go - Part 3  (0) 2023.08.13
Effective Go - Part 2  (0) 2023.08.10
Effective Go - Part 1  (1) 2023.08.09