💡 CSP : Communicating Sequential Processes
Go를 잘 활용하려면 Go가 동시성(concurrency)을 다루는 방법을 이해해야 한다.
동시성은 하나 이상의 작업을 동시에 진행하는 것을 의미하는데, 특히 Go에서의 동시성은 Go가 가지는 큰 강점 중 하나이다. Go에서의 동시성은 CSP 모델을 기반으로 한다. CSP는 동시성 시스템들 사이에서 일어나는 상호작용을 묘사하는 기본적인 모델 중 하나이다. (ex. 액터, stm 등)
기존의 프로그래밍 언어는 코드를 동시적으로 실행시킬 때, 쓰레드들과, 쓰레드들이 나눠가질 데이터 구조, 변수, 메모리 등을 파악하고 자원 관리를 위해 뮤텍스, 세마포어 등의 동시성 객체를 이용한다. 그러다보니 2개 이상의 쓰레드가 한 공유자원에 동시에 접근하는 것은 불가능 하게 된다. 이와 같은 모습을 쓰레드끼리 communicate한다고 표현하는데, 알다시피 이는 레이스 컨디션, 메모리 매니지먼트, 알 수 없는 예외, 데드락 등을 일으킨다.
그러므로 위와 같은 부작용을 배제하기 위해 Go는 다른 communication을 기본 모델로 삼았다. 공유 메모리 변수에 lock을 거는 대신, 하나의 쓰레드에서 다른 쓰레드로 변수에 저장되어진 값을 send(communicate)한다. 결국 위 모델의 기본 행동은 데이터를 보내는 쓰레드와 데이터를 받는(도착할 때까지 기다리는) 쓰레드이다.
안정적인 상태가 된다는 것은, send
쓰레드와 recv
쓰레드가 전송이 완료될 때까지 다른 행위를 하지 않는다는 것에 달려있다. 이는 레이스 컨디션과 같은 문제가 발생할 기회가 없어진다는 의미이기도 하다. 즉, 하나의 쓰레드가 어떤 행위를 완료하기 전까지는 다른 쓰레드가 행동 할 상황을 만들지 않는다는 말과 같다. 이와 같은 순차적 통신이 기본 모델인 셈이다.
Go는 이러한 순차적 통신 행위를 할 수 있게 여러 기능을 지원하는데, 중요한 점은 라이브러리 차원에서의 지원이 아니라, 언어 차원에서의 지원이라는 점이다. buffered channel
이라는 것을 지원하는데, 이를 통해 전송이 완료되는 동안 쓰레드들 간에 어떤 락이나 sync
를 맞출 필요가 없다는 뜻이다. 대신 두 개의 쓰레드 사이에 미리 정해진 멤버를 조작할 때는 sync/lock
할 수는 있다.
Go를 통해 동시성 코딩을 하려면 goroutine
과 channel
을 이용하면 된다.
goroutine
이라는 것은 위에서 설명한 쓰레드 개념을 제공하는데, 실제로는 쓰레드가 아니라 동일한 주소 공간에서 다른 goroutine
과 동시에 실행할 수 있는 기능이다. goroutine
은 OS의 쓰레드와 1대1 매칭되지 않고, 다중화되어 있기 때문에 논리적 쓰레드 혹은 가상 쓰레드라고 불리기도 한다. 모든 동기화 및 메모리 관리는 기본적으로 Go언어가 런타임 상에서 수행한다. goroutine
을 시작하려면 아무 함수에나 go 라는 키워드를 사용하면 된다.
package main import ( "fmt" "time" ) func say(s string) { for i := 0; i < 10; i++ { fmt.Println(s, "***", i) } } func main() { // 함수를 동기적으로 실행 say("Sync") // 함수를 비동기적으로 실행 go say("Async1") go say("Async2") go say("Async3") // 3초 대기 time.Sleep(time.Second * 3) }
go 채널은 Go에서 동시성을 실현하는 또 다른 핵심 개념이다. CSP에서 강조하는 통신 역할을 담당하기 때문이다. 즉 go 채널은 goroutine들 사이에 데이터를 전달하는 역할로 사용되고, 채널은 'make()' 함수를 통해 생성할 수 있다. 채널을 통해 데이터를 주고 받을 때는 채널 연산자 <-를 사용한다. goroutine은 다음과 같이 채널에 값을 할당한다.
package main
func main() {
// 정수형 채널을 생성한다
ch := make(chan int)
go func() {
ch <- 123 //채널에 123을 보낸다
}()
var i int
i = <- ch // 채널로부터 123을 받는다
println(i)
}
go 채널은 2가지의 채널이 있는데, Unbuffered Channel과 Buffered Channel이 있다. Unbuffered Channel은 하나의 수신자가 데이터를 받을 때까지 송신자가 데이터를 보내는 채널에 묶여있게 된다. 하지만 Buffered Channel에서는 비록 수신자가 받을 준비가 되어있지 않을 지라도 지정된 버퍼만큼 데이터를 보내고 계속 다른 일을 수행할 수 있다. Buffered Channel은 'make(chan type, N)'함수를 통해 생성할 수 있으며, 두번째 파라미터 N에 사용할 버퍼 갯수를 넣는다.
package main
import "fmt"
func main() {
ch := make(chan string, 1)
sendChan(ch)
receiveChan(ch)
}
func sendChan(ch chan<- string) {
ch <- "Data"
}
func receiveChan(ch <-chan string) {
data := <-ch
fmt.Println(data)
}
위와 같이 값을 받는 goroutine은 채널에서 그것을 추출하여 'data'라는 새로운 변수에 할당한다.
Go는 goroutine과 channel을 이용하여 CSP모델을 구현하려 했다. goroutine들 끼리 채널을 이용해서 자원을 전달하여 처리하는 것이 해당 내용의 핵심이었다.
"Do not communicate by sharing memory; instead, share memory by communicating"
공유 메모리에 의해 communicate 하지 마라.
그러므로 대신해서 communicating에 의해 메모리를 나눠라.
쓰레드 들의 '기다림'은 쓰레드들 사이에 교환이 일어날 때 더 적합한 씽크를 하게 만든다??
(이부분 이해를 못했음)
얘네가 진짜 스레드가 아닌 이유는 항상 병행적으로 행동되지 않기 때문이다. 하지만 멀티플렉싱 및 동기화로 인해 동시적으로 동작이 발생한다고 볼 수 있다.
공통 관심 사항(자원)에 대해 메모리를 나누어 가지면서 서로 접근을 통해서 처리하는 것이 아니다. ? 그럼 어캐하는데요 쉬발
👍🏻👍🏻👍🏻👍🏻👍🏻