컴포지트(composite) 디자인 패턴과 이름이 헷갈린 컴포지션은 UML 측면에서는 연관(Aggregation) 하고도 헷갈리기도 하는데 이 글에서는 컴포지션과 연관을 구분하지 않겠다. 컴포지션이란 간단히 말해 내가(객체)가 가지고 있어야 하는 특성을 외부에서 가져오는 것을 말하는데, 가져오는 방식이 상속을 통하는 방법과 다르게 외부에서 주입되는 방식이다. 상속의 경우는 폴리모피즘이 반드시 필요하면 제한적으로 사용하되, 컴포지션을 통해서 객체를 구축하는 방식을 추천한다. 

이 글에서는 Golang에서 컴포지션을 어떻게 지원하는지 살펴 볼 것이다. 자바/C++ 보다는 훨씬 깊숙히 임베디드 되는 모습을 보게 될 것이란 것을 미리 귀뜸해 둔다.

먼저 자바의 컴포지션을 살펴보자. 

abstract class Form{
public void run(){};
}
class WolfForm extends Form{
public void run (){
System.out.println("늑대처럼 달린다!!!");
}
}
class Player{
public String name;
public Form form;
public Player(String name, Form form){
this.name = name;
this.form = form;
}
}
public class Main {
public static void main(String[] args) {
Player player = new Player("hama", new WolfForm());
player.form.run();
}
}

Player 객체는 Wolf 폼 객체를 포함(컴포지션)하고 있으며, Player객체에서 Wolf폼 객체의 기능을 사용하려면 당연히player.wolf.run() 식으로 내부에서 wolf 로 접근한 후에 사용한다. 근데  Golang에서는??
그냥 player.run()으로 접근가능하다.

Golang에서의 컴포지션 

*  객체/값의 구분을 하지 않았음을 알려드린다. 이 글에서는 포인팅되는지 값 자체인지의 구분이 중요치 않다.

type Form interface{
Run()
}
type WolfForm struct{
}
func (wolf *WolfForm) Run() {
fmt.Print("늑대처럼 달린다!!!");
}
type Player struct{
Form
}
func main(){
player := &Player{Form : &WolfForm{}}
player.Run()
}

Player 구조체는 Form을 혼연일체 (임베디드) 되어 가지고 있게 된다. 즉 자신의 메소드인 양 그냥 사용 해 버릴수 있다. 어찌보면 강력해 보이기도 하는데, 이게 코드읽기/관리가 될 때는 좀 피곤 할 수 있을 것이다. 
자바의 경우 player.wolf.run()으로 관계가 명시적으로 코드에 보이는 반면에, Golang식의 player.run()은 도대체 run은 어디에 구현되어있는지 알수 없으면, 찾아보는 수고를 해야한다. player 내부의 Form 인터페이스를 확인해야하며, 이 Form인터페이스를 덕타이핑으로 구현상속한 녀석을 또 찾아봐야한다. player 내부에 Form인터페이스 말고 다른것들도 있다면?? 두배로 힘들어지게 된다. 아래 이더리움 코드를 통해 피부로 느껴보자.

이더리움코드에서의  컴포지션 
[이더리움 코어] DevP2P 소스코드 분석 (feat. golang) 를 같이 읽으면  좋을 거 같다.

이더리움에서 P2P 부분을 보면 리모트서버와 접속이 이루어짐과 동시에 conn 객체가 만들어 지는데,
conn 구조체는 아래와 같다. 위에 공부해 보았듯이 transport 가 컴포지션 되었다는 것을 알 수 있다.

type conn struct {
fd net.Conn
transport
flags connFlag
cont chan error
id discover.NodeID
caps []Cap
name string
}

conn 객체에는 net.Conn이라는 소켓파일디스크립터과 그 fd 를 가지고transporting 하는 방식을 책임지는 transport 인터페이스를 구현상속한 녀석을 가지고 있다. 그럼 transport는 무엇을 하는 것일까? transport 인터페이스를 살펴보자.

type transport interface {
doEncHandshake(prv *ecdsa.PrivateKey, dialDest *discover.Node)(discover.NodeID, error)
doProtoHandshake(our *protoHandshake) (*protoHandshake, error)
MsgReadWriter
close(err error)
}

리모트노드와 접속이 이루어지면 Encrypt와 Protocol 정보를 교환해야하는 책임을 가지고 있으며, 
MsgReadWriter 인터페이스를 구현상속한 또 어떤 녀석을 통해서 리모트노드와 읽고,쓰기를 하는거 같다.
이더리움에서는 메세지를 시리얼라이즈 하는데  RLP 라는 인코딩/디코딩 알고리즘을 사용하는데, MsgReadWriter 인터페이스를 구현한 녀석은 RLP의 능력도 가지고 있다. 

이제 conn 객체의 생성 코드를 살펴보자. 확인해 볼것은 2가지인데 하나는 transport 인터페이스는 어떤 객체로 만들어지나이고,  transport 인터페이스가 가지고 있는 MsgReadWriter 인터페이스 또한 어떤 객체로 만들어 지나이다.

func (srv *Server) SetupConn(fd net.Conn, flags connFlag, dialDest *discover.Node) error {
...
c := &conn{fd: fd, transport: srv.newTransport(fd), flags: flags, cont: make(chan error)}
...
}

transport 에 srv.newTransport(fd) 가 컴포지션 되고 있다.

srv.newTransport = newRLPX

func newRLPX(fd net.Conn) transport {
return &rlpx{fd: fd}
}

rlpx 객체를 만드는 것 같다. (내부에 fd 를 가지고 있으며, rlp 엔코딩/디코딩으로 시리얼라이즈를 한 후에 fd로 소켓read/write호출)  

type rlpx struct {
fd net.Conn

rmu, wmu sync.Mutex
rw *rlpxFrameRW
}

rlpx 구조체는 위와 같으며, 이 구조체는 transport 인터페이스가 가지고 있는 메소드를 모두 덕타이핑(자바처럼 extends로 명시적으로 상속한다고 말하지 않아도 동일한 함수시그니처를 구현하고 있으면 폴리모피즘적으로 같다라고 봄) 으로 포함 할 것이다. 그래야만 rlpx 가 transport가 될 수 있으니~ 아래와 같이~

func (t *rlpx) doProtoHandshake(our *protoHandshake) (their *protoHandshake, err error) {

....서로가 가진 프로토콜 정보 확인 ...난 eth63을 가지고있는데 넌 무엇을?
}

func (t *rlpx) doEncHandshake(prv *ecdsa.PrivateKey, dial *discover.Node) (discover.NodeID, error) {
....서로가 가진 암호화 정보 확인 ECC기반...auth정보 교환..
t.rw = newRLPXFrameRW(t.fd, sec)
...
}

마지막으로 transport 인터페이스에서 선언한 MsgReadWriter 또한 transport를 구현상속한 rlpx 가 가지고 있어야 하는데 어디 있을까? 다시 rlpx 구조체를 보자.

type rlpx struct {
fd net.Conn

rmu, wmu sync.Mutex
rw *rlpxFrameRW
}

rlpxFrameRW가 뭔가 읽고,쓰기와 관련되어 있는 감이 오지 않나? (자바라면 명시적으로 보였을텐데..) 
그럼rlpxFrameRW가 생성되는 곳을 살펴봐야한다. 이것은 위의 doEncHandshake 메소드 안에 있다.

t.rw = newRLPXFrameRW(t.fd, sec)

newRLPXFrameRW 는 이렇다.

func newRLPXFrameRW(conn io.ReadWriter, s secrets) *rlpxFrameRW {
...
return &rlpxFrameRW{
conn: conn,
enc: cipher.NewCTR(encc, iv),
dec: cipher.NewCTR(encc, iv),
macCipher: macc,
egressMAC: s.EgressMAC,
ingressMAC: s.IngressMAC,
}
}

rlpxFrameRW 라는 구조체를 만든다. 이쯤되면 감 잡힐 것이다. 이 구조체는 MsgReadWriter 인터페이스를 구현 상속 했다는 것을..(RW라는 이름을 통해서 유추해야한다. 따라서 Golang에서 이름짓기는 더 중요할 것이다) 

type MsgReadWriter interface {
MsgReader
MsgWriter
}}

type MsgReader interface {
ReadMsg() (Msg, error)
}
type MsgWriter interface {
WriteMsg(Msg) error
}

MsgReadWriter 인터페이스는 MsgReader/MsgWriter라는 인터페이스를 또  가지고 있으며, rlpx 에서는 ReadMsg/WriteMsg만 덕타이핑해 놓으면 될 것이다. 아래 코드에서 확인하자.

func (t *rlpx) ReadMsg() (Msg, error) {
t.rmu.Lock()
defer t.rmu.Unlock()
t.fd.SetReadDeadline(time.Now().Add(frameReadTimeout))
return t.rw.ReadMsg()
}

func (t *rlpx) WriteMsg(msg Msg) error {
t.wmu.Lock()
defer t.wmu.Unlock()
t.fd.SetWriteDeadline(time.Now().Add(frameWriteTimeout))
return t.rw.WriteMsg(msg)
}

이렇게 이더리움에서 컴포지션이 이루어지는 모습을 살펴보았다. 어떻게 느껴졌는가?? 강력하다? 헷갈린다?? 모든 기술은 장,단이 있으며 사람의 취향에 따라서 달라질 것이다. 자바스러움이 좋은 사람, Golang스러움이 좋은 사람~개인적으로는 자바스러움이 좋긴한데, Golang의 동시성  철학과 빠른컴파일속도에 점수를 주어서 Golang에 집중하고 있다. 언어 선택에 더 중요한것은 내가 지금 하는 일에 어떤 언어가 더 가깝냐는것이 겠지만~




chan chan 이란?

블록체인을 비롯해서 서버쪽 개발 언어로 go 언어가 엄청나게 부상하고 있는데요. 이 글에서는 go 언어의 가장 큰 특징 중 하나인 고루틴/채널에 대하여 간단히 설명하며, 좀 더 고급기술인 채널 위에 채널을 얹는 문법을 이더리움 코드를 통해서 살펴봅니다.

golang은 C,C++ 보다 실전적으로 고성능이라고 생각하는데, 그 근거가 바로 고루틴과 채널입니다. 물론 C++도 쓰레드가 있긴 하지만 코딩 할 때, 아~ 이 부분은 좀 시간 걸리겠는데 하더라도 쓰레드로 빼긴 부담스럽게 느껴지는게 사실이며, 많은 C++ 개발자들이 그러한 습관에 코딩을 하고 있습니다. 하지만 GO언어는 언어자체에서 경량쓰레드를 지원하므로, 너무나도 자연스럽게 쓰레드화하고, 채널을 통해서 그것의 결과에 해당하는 값을 받아 볼 수 있게 하고 있습니다.CPU를 너무나 적극적으로 사용하고 있는 이 과정이 너무 쉽기 때문에 golang 으로 짜여진 오픈소스를 보면 고루틴,채널이 범벅되어 있는 것을 많이 볼 수 있습니다. (아무리 쉽다고 해도 고루틴,채널 범벅이면 시리얼한것보다는 복잡해 보이는것은 사실입니다) 혹시 고루틴과 채널에 대해서 처음 듣는 것이라면 제가 예전에 번역한 고 언어에서의 동시성 모델(CSP) 를 먼저 읽고 이 포스트를 보시면 좋을 거 같습니다.

(혹시 고성능이 무조건 최고다라고 생각하시는 분은 없었으면 합니다. 0.0000001초가 걸리던 0.00001초가 걸리던, 일반적인 현실에서는 0.01초만 걸려도 아무 상관없는 솔루션이 대부분이기 때문입니다. 성능보다는 다른 지표로 언어를 선택해야하는 경우가 더 많습니다.그래서 파이썬이 잘나가는거겠지요.^^) 

고루틴

경량쓰레드인 고 루틴을 만드는것은 너무나 간단하다. 로직을 짜다가 이것은 조금 시간이 걸리겠다 싶으면 그냥 고루틴으로 만들 수 있다. 


func main()
{
go doSomthing() // GO 루틴 시작

}

func doSomthing()
{
.... 무엇인가 한다 ...
}


이렇게 일반 함수 앞에 go 라고 붙히기만 하면 경량 쓰레드로 동작한다.

채널

저렇게 고루틴을 만들고 나면, 고루틴에서 어떤 작업을 했을 때, 그 결과를 받고 싶을 수가 있다. 보통 다른 언어로는 동시성을 보장하는 큐를 만들어서 매개변수로 보내서 처리 할 텐데, (혹은 이벤트 동기화) 고 언어에서는 더욱 간편한 것이 있다. 이걸 채널이라고 부르는데 이거 덕분에 고언어에서 쓰레드를 사용하는 것은 더욱 쉬워 지며, 안정적이게 된다. Mutex 를 사용하지 않고 액터패턴처럼 구현되기 때문인데 아래에 예를 살펴보자.


func main()
{
ch := make(chan int) // 통신용 채널 생성

go sendingGoRoutine(ch) // GO 루틴 시작
go receivingGoRoutine(ch) // GO 루틴 시작
..
}
// 송신 코루틴
func sendingGoRoutine(ch chan int)
{
... 어떤 작업을 함 ...

ch <- 45 // 결과 값을 보냄.
}
// 수신 코루틴
func receivingGoRoutine(ch chan int)
{
// 송신 채널로부터 값을 올때까지 기다리다가 오면, v 에 입력
v := <- ch
fmt.Println("Received value ", v)
}

ch := make(chan int) // 통신용 채널 생성

채널은 위와 같이 만들며 int 타입을 전송할 수 있는 채널을 만든 다는 것이다.
생산자가 ch 에 어떤 값을 쓰면, 소비자는 ch 을 기다리다가 값이 들어오면 처리한다.
위의 예에서 생산자는

func sendingGoRoutine(ch chan int)
{
   ...
  ch <- 45
}
이것이 되며, ch <- 45 , 즉 채널에 45를 송신해 주고 있다.
소비자는
func receivingGoRoutine(ch chan int)
{
   // 채널로부터 값을 받으면 v 에 입력
   v := <- ch
   fmt.Println("Received value ", v)
}
이것이 되며, v := <-ch , 즉 채널로부터 값을 수신 받기를 기다리다가 값이 들어오면 처리한다.


chan chan 이란?

requestChan := make(chan chan string)

자 채널에 대해서 공부해 봤는데. chan chan 이렇게 두개를 연속으로 사용 할 수도 있다.무엇 일까?
채널에 값을 보내는게 아니라, 채널에 채널(문자열을 통신하는)을 보내고 있다. 즉
chan 이 "보내면 받을께" 라면 
chan chan 은 "내가 준비됬다는것을 알려줄께 기다리다, 내가 보낸 채널을 통해 보내" 이다. 양방향 처리를 할 수 있다.
아래 그림을 보자. 고루틴 C (소비자 역할)는 고루틴 D (생산자 역할) 에게 "내가 준비 됬다는 것을 chan chan채널을 통해서 알려주면, 채널을 통해 내가 보낸 채널을 통해 니가 나한테 생산한것을 건네줘" 라고 하는 것을 도식화 한 것이다.

Screenshot

1. 고루틴 C 와 고루틴 D은 chan chan 채널(requestChan := make(chan chan string)을 함께 가지고 있다.

Screenshot

2. 고루틴 C (소비자) 는 처리 할 준비가 되었다고, 알려주며 이 채널 (여기선 response 채널)을 통해서 보내라고 고루틴 D에게  말해준다.

Screenshot

3. 고루틴 D (생산자) 는  고루틴 C에게 받은  채널 (여기선 response 채널)을 통해서 고루틴 C에게 문자열을 보낸다.

package main

import "fmt"
import "time"

func main() {

requestChan := make(chan chan string)

go goroutineC(requestChan)
go goroutineD(requestChan)

time.Sleep(time.Second)

}

func goroutineC(requestChan chan chan string) {

responseChan := make(chan string)

requestChan <- responseChan

response := <-responseChan

fmt.Printf("Response: %v\n", response)

}

func goroutineD(requestChan chan chan string) {

responseChan := <-requestChan

responseChan <- "wassup!"
}

소스를 통해 명확히 이해 할 수 있을 것이다. 키포인트는
requestChan <- responseChan  채널에게 값을 전송하는게 아니라, 채널을 전송하는 지점이다. 

이더리움 오픈소스에서 chan chan 은 어떻게 사용 되었나? (소스)

이더리움에서 p2p 시작 부분을 살펴보면, (kademlia 알고리즘에 따라) 다른 노드를 찾기 위한 요청을 하기 전에 자신의 버켓을 다시 채우는(리프레쉬)과정이 있는데, 여기서 리프레쉬를  고루틴(쓰레드)화 해서 처리하고, 리프레쉬가 끝나면 끝났다고 알려주길 바라는 부분이 있다. 이때 chan chan을 통해서 처리하는데, 소스를 통해 확인해 보자.

refreshReq: make(chan chan struct{}),

 refreshReq 라는 chan chan을 만듭니다. 변수 이름이 말해 주듯이 리프레쉬를 요청하는 채널인데, 위의 예제와 마찬가지로 이 채널에 response에 해당하는 채널을 건네주고, 대기 하는 로직 일 수도 있겠다.

func (tab *Table) refresh() <-chan struct{} {
    done := make(chan struct{})
    select {
    case tab.refreshReq <- done: <-- 이 부분
    case <-tab.closed:
        close(done)
    }
    return done
}

response 는 아니고, done 이라는 채널을 만들어서 건네주고 있다. 리프레쉬를 한 다음에 완료되면 done 채널을 통해 보내라는 것 같다. 그럼 누가 받을 까? 리프레쉬를 진짜 실행하는 놈이 받겠지. 그리고 리프레쉬 하는 놈이 done에 어떤 것을 보내면 그것을 처리 하기 위해 위의 return done 을 받은 녀석은 어디선가 대기하고 있을 것이다.


// loop schedules refresh, revalidate runs and coordinates shutdown.
func (tab *Table) loop() {
    ...
    for {
        select {
  
        case req := <-tab.refreshReq: <--- 이 부분
            waiting = append(waiting, req)
            if refreshDone == nil {
                refreshDone = make(chan struct{})
                go tab.doRefresh(refreshDone)
            }
       ...

loop 함수에서 req := <-tab.refreshReq:  이렇게 받고 있다. 받은 것을 waiting 배열에 넣어서 보관하며
진짜 리프레쉬를 하는 go tab.doRefresh(refreshDone)  함수를 호출하고 있다.

    case req := <-tab.refreshReq:
            waiting = append(waiting, req)
            if refreshDone == nil {
                refreshDone = make(chan struct{})
                go tab.doRefresh(refreshDone)
            }
        case <-refreshDone: <--- 이 부분
            for _, ch := range waiting {
                close(ch) <--- 이 부분
            }
            waiting, refreshDone = nil, nil

go tab.doRefresh(refreshDone) 부분을 보면, refreshDone 이라는 채널을 매개변수로 넣어주는데, 예상하듯이 리프레쉬가 완료되면 이 채널로 완료 보고를 하라는 것이고,  case <-refreshDone:  <--- 이 부분 여기서 완료 보고를 기다리고 있다.
그 아래 close(ch)가 있는데, 여기서 진짜 처음 소스에 있는 done := make(chan struct{})이 done에 대한 완료 보고를 하고 있다. 즉 모든 처리가 끝났다고 알려 준다. 그럼 done 을 보고 받는 마지막 코드를 보자.

func (tab *Table) lookup(targetID NodeID, refreshIfEmpty bool) []*Node {
....
    for {
        tab.mutex.Lock()
        // generate initial result set
        result = tab.closest(target, bucketSize)
        tab.mutex.Unlock()
        if len(result.entries) > 0 || !refreshIfEmpty {
            break
        }
        // The result set is empty, all nodes were dropped, refresh.
        // We actually wait for the refresh to complete here. The very
        // first query will hit this case and run the bootstrapping
        // logic.
        <-tab.refresh() <--- 여기 대기
        refreshIfEmpty = false
    } .. look up 처리 ..

위의 코드를 보면 tab.refresh() 를 호출하고 리프레쉬가 완료되길 기다리고 있다. 즉 리프레쉬가 완료 된 후에야 lookup을 처리하겠다는 것이다. 

소스를 보면 아시다시피, 고 언어에서 고루틴/채널은 정말 너무 사소하게 사용 된다. 그러다보니, 채널의 채널이 생기고 그 그 채널을 처리 하기 위해 또 고루틴을 만들고 그것을 위한 채널을 또 만들고...멀티코어 를 적극적으로 사용하는 모습이 기본이며, 그래도 고 언어이기 때문에 그나마 복잡도가 많이 줄어 든거 같긴 하다. :-) 



레퍼런스:
http://tleyden.github.io/blog/2013/11/23/understanding-chan-chans-in-go/
https://github.com/ethereum/go-ethereum/blob/master/p2p/discover/table.go



임백준님이 번역하신 "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

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

+ Recent posts