CSP모델을 통한 Go 언어의 특성

Damon Kwon·2022년 1월 22일
1

Go를 아시나요

목록 보기
2/3
post-thumbnail

💡 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를 통해 동시성 코딩을 하려면 goroutinechannel을 이용하면 된다.

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들 끼리 채널을 이용해서 자원을 전달하여 처리하는 것이 해당 내용의 핵심이었다.


이해 못했던 내용들

  1. 메세지 전달을 잘한다 → 분산을 잘한다
    액터나 CSP처럼 메세지 전달을 기반으로 삼는 테크닉이 중요한 역할을 할 것이다.
    Go의 동시성은 단순 문법적인 요소가 아니다.
  1. Go에서 동시성을 설명할 때 주로 사용되는 문구는 아래와 같다.

"Do not communicate by sharing memory; instead, share memory by communicating"

공유 메모리에 의해 communicate 하지 마라.
그러므로 대신해서 communicating에 의해 메모리를 나눠라.
쓰레드 들의 '기다림'은 쓰레드들 사이에 교환이 일어날 때 더 적합한 씽크를 하게 만든다??
(이부분 이해를 못했음)

  1. 경량화 된 스레드이다. 이것들은 OS 스레드 사이에 다중화(multiplexed) 되어있는데, ??하나의 블록이 있다면 다른 것들은 계속 진행될 수 있다??

얘네가 진짜 스레드가 아닌 이유는 항상 병행적으로 행동되지 않기 때문이다. 하지만 멀티플렉싱 및 동기화로 인해 동시적으로 동작이 발생한다고 볼 수 있다.

공통 관심 사항(자원)에 대해 메모리를 나누어 가지면서 서로 접근을 통해서 처리하는 것이 아니다. ? 그럼 어캐하는데요 쉬발

참고

예제로 배우는 Go 프로그래밍 - Go 루틴 (goroutine)

고 언어에서의 동시성 모델

profile
👽 DevMyong, 신입 백엔드 개발자 🌊 myong.dev@gmail.com

3개의 댓글

comment-user-thumbnail
2022년 1월 22일

👍🏻👍🏻👍🏻👍🏻👍🏻

1개의 답글
comment-user-thumbnail
2022년 9월 18일

좋은글 감사합니당

답글 달기