관리 메뉴

HAMA 블로그

고 언어에서의 동시성 모델 본문

Go

고 언어에서의 동시성 모델

[하마] 이승현 (wowlsh93@gmail.com) 2018. 2. 27. 17:02



임백준님이 번역하신 "7가지 동시성 모델" 책에는 순차 프로세스 통신 (CSP) 이라는 내용이 있는데  Golang 에서 구현한 모델을 클로저언어로 래핑한 라이브러리를 이용해서 설명하고 있다. 역시 책에 나오는 내용 "미래는 불변이다", "미래는 분산이다" 라는 구절이 있다. 분산을 잘하기 위한 도우미로 "메세지 전달" 이 매우 중요한데, "액터" 나 "CSP" 처럼 메세지 전달을 기반으로 삼는 테크닉이 점점 더 중요한 역할을 하리라 예측하고 있다. 이번 번역 글 (중간 중간 동시성에 대한 개인적인 견해가 많이 들어가 있다) 에서는 Golang에서의 CSP 에 대해서 살펴본다. 학술적인 내용이 아니며 아주 기초적인 내용을 짧게 담고 있는데, 액터에 대해서 알고 있는 분이라면 통신하는 객체 자체(액터) 보다 "통신" 그 자체에 방점을 찍는게 CSP 라고 이해하고 읽어보면 편 할 것이다.



Concurrency in Golang

[번역] http://www.minaandrawos.com/2015/12/06/concurrency-in-golang/


어제, Quora 에서 고 언어에서의 동시성 모델에 대한 답변을 달아주었는데, 좀 더 말 할게 있겠다는 생각이 들어. 고 언어에서의 동시성은 고 언어를 빛나게 하는 가장 강력한 것 중 하나거든. 많은 사람들이 이것에 관해 간단한것 부터 시작해서 복잡한 내용에 이르기까지 말해 왔었는데, 이번에는 내 생각을 말 할 차례가 온 거 같아.

고 언어에서의 동시성은 단지 문법적인 요소라고 생각 하기에는 좀 더 깊이 있게 생각해 볼 만한 것들이 있어. 고 언어의 파워를 잘 활용하기 위해서는 일단 어떻게 고 언어가 동시성을 다루는지에 대해 잘 이해하고 있어야 하지. 고 언어는 CSP (Communicating Sequential Processes : 순차적 프로세스들의 통신) 라 불리는 동시성 모델을 사용하는데,이것은 컴공에서 동시성 시스템들 사이에서 일어나는 상호작용을 묘사하는 아주 기본적인 모델이야. 근데 이 글이 과학논문은 아니기 때문에 아주 구체적인 내용에 대해서는 일단 건너 뛰려해

고 언어에서의 동시성을 설명 할 때 주로 사용되는 문구는 다음과 같지.

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

공유 메모리를 이용하여 커뮤니케이션 하지말고, 커뮤니케이션에 의해 메모리를 나누자.

뭐 좋은 소리 같긴 하다 ㅎㅎ  근데 이게 의미하는게 무엇일까? 커뮤니케이션에 의해 메모리를 나누자-라니..  내 머리속에서 개념들이 휘몰아 치다가 한순간에 정리 됬어. 알버트 아인슈타인은 이런 말을 했었거든 "만약 당신이 초딩도 알수 있게 끔 간단히 설명 할 수 없다면, 당신은 진정 그것을 이해한다고 볼 수 없습니다" 나는 이제 정말 간단히 설명할 수 있을 거 같아. 

(역주 :  쓰레드들이 공유 될 것들을 동시간에 접근하는게 아니라, 서로 전달하는 방식임. 여기서 함정(?)이 Go 언어에서 다른 언어처럼 공유변수를 서로 접근하는 방식을 사용 안하는건 아니며 서로 공유변수를 접근하는 방식을 사용하라고 sync 관련 키워드도 열라 많음. 자바나 스칼라도 STM,CSP,액터를 기본 동시성 객체와 함께 사용 할 수 있듯이~~차이점이라면 Go 언어는 언어 자체에서 CSP 를 지원한다는 점. 참고로 클로저언어는 STM 을 내장한다) 

진짜 내가 이해했는지 증명해보지. 자! 시작해 보자고~

공유 메모리에 의해 communicate 하지 말라!

주로 프로그래밍 언어에서는 코드를 동시적으로 실행시키는 것에 대해 생각 할 때,  먼저 여러개의 쓰레드들을 떠올릴 거야. 그것들이 복잡하게 구현되면서 병렬적 수행을 하는 것 말이지. 그리고 쓰레드들이 서로 나누어 가질 데이터 구조/변수/메모리/등등이 무엇인지 파악할 것이고, 이것들을 사이 좋게 나누어 갖게 하기 위해 동시성 객체(뮤텍스등)을 이용할 테지. 그 결과 2개의 쓰레드가 동시간에 한 군데에 쓰는 작업을 할 수 없을 테고 ~ 뭐 그냥 알아서 잘되겠지 하고 놔두거나, 아예 인지도 못할지도 모르지. 이런것이 전형적인 어떻게 다른 쓰레드들끼리 "communicate" 하는가에 관한 것일 꺼야. 아시다시피 이런 행위는 결국 레이스 컨디션, 메모리 매니지먼트, 알수 없는 예외, 데드락 등을 일으키는거지. 예외 없을꺼야. 대규모 동시성 프로그래밍을 C++ 나 Java 등으로 해본 사람들이라면 잘 알것이야.

대신해서, communicating 에 의해 메모리를 나누어라.

어떤 아이디어로 고 언어는 이걸 수행 할까? 공유 메모리 변수에 대해 락을 거는 대신해서 고 언어는 하나의 쓰레드에서 다른 하나(실제로는 우리가 알고있는 그 쓰레드는 아니지만 지금은 그렇게 생각하자고) 로 변수에 저장되어진 값을 communicate (or send) 하게 만들어줘. 기본 행동은 데이터를 보내는 쓰레드와 데이터를 받는(도착할때까지 기다리는)쓰레드야. 쓰레드들의 그 "기다림"은 쓰레드들 사이에 교환이 일어날 때 더 적합한 싱크를 하게 만들지.

좀 더 선명하게 말하자면, 안정적인 상태라는 것은 보내는 쓰레드와  받는 쓰레드가 전송이 완료 될 때까지 아무것도 안한다는데에 있어. 레이스 컨디션이라든지 비슷한 문제가 발생할 기회가 없어지게 한다는 거지. 즉 하나의 쓰레드가 어떤것을 완료하기 전에 다른 쓰레드가 행동 할 상황을 만들지 않는거야. 단순하지

고 언어는 이런 순차적 통신 행위를 할 수 있게 하는 여러 기능들을 지원하는데  중요한 것은 라이브러리 차원이 아니라 언어 차원  라는 거야. 엄청 간단하지. "buffered channel" 이라는 것을 지원하는데 이것에 의해 전송이 완료 될 때까지 쓰데드들 간에 어떤 락이나 sync 를 맞출 필요가 없어. 

대신 두개의 쓰레드들 사이의 미리 정해진 멈버를 조작 할 때는 싱크로니제이션/락킹을 할 수 는 있겠다. 간련된 뮤텍스 예제는 여기를 참고해: sync.Mutex  (역주1: 다시 언급하지만 이러한 문제가 있기 때문에 golang 도 동시성에 여전히 위험한 언어라고 생각된다. 아예 불변이 디폴트인 클로저나 얼랭이 이런 면에서는 괜찮긴 한데.. 여러모로..선택을 주저하게 만들고,..따라서 동시적 행위가 비교적 단순한 것은 golang ,좀 복잡하고 유기적이면 스칼라/아카를 선택하게 만들고 있다. 참고로 파이썬은 이상하게 멀티쓰레드,멀티프로세스를 사용하는데 문제가 많더라고.몇일후 혹은 한달후에 갑자기 작동을 안한다던지 하는..2.7 에서 3.6으로 바꾸니깐 그런 현상이 사라지기도 하고..따라서 파이썬에서 Golang으로 갈아탔다.)


(역주2: 불변은 쓰레드세이프하다? CPU 여러개를 활용하는 동시성이 뜨면서  불변이 강조되며, 불변=동시성=함수형프로그래밍이 라는 단어가 항상 함께 나열되다보니, 그럼 불변이면 쓰레드 세이프 한거야? 라고 생각 할 수 있는데 광의로 보면 그렇지 않습니다. 쓰레드세이프한 솔루션이 되지는 않습니다. 주의해야합니다.) 

고 언어를 이용한 동시성 코딩 

도대체 어떻게 한다는 것일까? 이제 코드를 보면 좀 더 명쾌해 질거야.

Go에서 "goroutine"은 위의 설명 한 스레드 개념을 제공하는데, 실제로는 스레드가 아니며 기본적으로 동일한 주소 공간에서 다른 goroutine과 동시에 실행할 수있는 기능이라고 볼 수 있지. 경량화된 쓰레드라고도 해. 그들은 O.S 쓰레드 사이에 다중화(multiplexed)되어 있는데 하나의 블록이 있다면 다른 것들은 계속 진행 될 수 있지. 모든 동기화 및 메모리 관리는 기본적으로 Go 언어에 의해 수행되며, 그들이 진짜 스레드가 아닌 이유는 항상 병행적으로 행동되지 않기 때문이야. 하지만 멀티플렉싱 및 동기화로 인해 동시적으로 동작이 발생 한다고 볼 수 있어. 

쓰레드와 비슷한  goroutine을 시작하려면 아무 함수에나 "go"라는 키워드를 사용하면 되. 간단하지~

go myfunction()

Go 채널은 Go에서 동시성을 실현하는 또 다른 핵심 개념이며 이 글의 주제이기도 하지. CSP 에서 강조하는 그 "통신" 역할을 담당하니깐. 즉 goroutine간에 메모리를 전달하는 데 사용되며 채널을 만들려면 "make" 를 사용하면되.

myChannel := make(chan int64)

goroutines이 대기하기 전에 더 많은 값을 대기열에 넣을 수 있도록 버퍼링 된 채널도 만들 수 있어.

myBufferedChannel := make(chan int64,4)

위의 두 예제에서는 채널 변수가 이 전에 생성되지 않았다고 가정했어. 그래서 ": ="을 사용하여 추론된 유형을 가진 변수를 만들었는데. ( "="는 값을 할당 할 뿐이며 변수가 이전에 선언되지 않은 경우 컴파일 오류가 발생한다.)

채널을 사용하려면 "<-"표기법을 사용하는데, 값을 (여기서 숫자 54) 보내는 goroutine은 다음과 같이 채널에 값을 할당하지.

mychannel <- 54

값을 받는 goroutine은 채널에서 그것을 추출하여 다음과 같은 새로운 변수에 할당해.

myVar := <- mychannel

이제 Golang에서 동시성을 보여주는 전체 예제를 살펴 보자고.

package main

import (
"fmt"
"time"
)
// 메인
func main()
{
ch := make(chan int) // 통신용 채널 생성
done := make(chan bool)

go sendingGoRoutine(ch) // GO 루틴 시작
go receivingGoRoutine(ch,done) // GO 루틴 시작
// 프로그램이 종료되는 것을 막음.
<- done
}
// 송신 코루틴
func sendingGoRoutine(ch chan int)
{
// 5초후에 메세지 달라는 코루틴 생성
t := time.NewTimer(time.Second*5)
// 5초후에 아무 메세지나 받음.
<- t.C

fmt.Println("Sending a value on a channel")
ch <- 45
}
// 수신 코루틴
func receivingGoRoutine(ch chan int, done chan bool)
{
// 채널로부터 값을 받으면 v 에 입력
v := <- ch
fmt.Println("Received value ", v)
done <- true
}

결과는 다음과 같어.

Sending a value on a channel
Received value 45

코루틴들끼리 채널을 이용해서 공통 관심 사항을 전달(커뮤니케이션)을 통해서 처리하는 것을 잘 기억하라고. 공통 관심 사항에 대해 메모리를 나누어 가지면서 서로 접근을 통해서 처리하는게 아니야

Comments