728x90

동시성 프로그래밍을하게되면 가장 크게 대두되는 문제점이 있다.

바로 동기화이다.


package main

import (
"fmt"
)

func sigma(start int, end int) int {
sum := 0
for i := start; i <= end; i++ {
sum += i
}
return sum
}

func main() {
var a int
var b int
a = sigma(1, 100)
b = sigma(101, 200)
fmt.Println(a + b)
}

가령 위 같은 코드를 보자. sigma는 특정 숫자로 부터 숫자까지 사이의 모든 숫자를 더하는 함수이다.

그래서 우리는 1부터 100까지 더하고 101부터 200까지 더하는 코드를 짰다고 가정하면 위 같은 코드가 될 것이다.

위의 코드를 고루틴으로 짠다면 어떻게 될까??

이 때까지 우리는 변수를 받아오지는 않았다. 고루틴을 사용해서 특정값을 받아오려면 return으로는 적절하지 않다.


이제 채널이 등판할 차례이다.


채널 => 데이터를 전달하는 통로

package main

import (
"fmt"
)

func sigma(start int, end int, c chan int) {
sum := 0
for i := start; i <= end; i++ {
sum += i
}
c <- sum
}

func main() {
var a int
var b int
//var c chan int= make(chan int)
//var c = make(chan int)
c := make(chan int)
go sigma(1, 100, c)
go sigma(101, 200, c)
a, b = <-c, <-c
fmt.Println(a + b)
}

변수중에는 특별한 변수가 있다.

바로 채널변수이다.


var c chan int= make(chan int)
var c = make(chan int)
c := make(chan int)

선언 하는 방법은 다른 변수들과 동일하다.

단 위의 방식들은 모두 버퍼의 크기가 0인 채널변수이다.

버퍼의 크기라는 개념은 아래에 언급하도록 하겠다.


채널변수는 다른 변수들과 다르다.

두가지의 연산자를 사용할 수 있다.


c <- sum
a, b = <-c, <-c

채널 변수에 들어가는 방향의 화살표 연산자, 그리고 나가는 방향의 화살표 연산자, 총 두종류를 사용할 수 있다.


들어가는 방향 - 해당 채널로 값을 보낸다.

나가는 방향 - 해당 채널의 값을 꺼낸다.


일반적으로 채널은 변수의 역활을 할 순없다. 통로 역활만 할 뿐이다.


채널은 말그대로 연결이며 송수신의 관계로 이해해야한다.

위의 코드를 바탕으로 흐름을 설명하도록 하겠다.


go sigma(1, 100, c)

이 코드로 인하여 고루틴이 생성된다.


a, b = <-c, <-c

여기서 a가 b보다 먼저 실행된다.

a는 채널 c로 부터 값을 받으려고한다.


func sigma(start int, end int, c chan int) {
sum := 0
for i := start; i <= end; i++ {
sum += i
}
c <- sum
}

또한 sigma함수에서는 c로 값을 보내려고한다.

이게 중요하다.


위의 코드는 사실 충분히 예측가능하다.

a,b에 값을 넣는 작업이 sigma계산 후 채널로 sum값을 보내는 작업보다 아주아주아주 높을 것이다.


다만 우리는 알수없다고 가정해야한다.

왜냐하면 대부분은 정말로 알 수 없기 때문이다.


즉 여기서 (1)a에 c로 값을 받는 작업, (2)c로 sum값이 전달되는 작업은 누가 먼저 일어날지 모른다고 봐야한다.

하지만 논리적인 흐름상 1번작업이 당연히 2번작업 다음에 이뤄줘야한다.

당연하다. sigma로 계산되어야만 a값이 의미를 가지지 않겠는가?

하지만 논리적인 흐름상 1번이 2번보다 먼저 도달은 할 것이다.


package main

import (
"fmt"
)

func sigma(start int, end int, c chan int) {
sum := 0
for i := start; i <= end; i++ {
sum += i
}
fmt.Println("sigma")
c <- sum
}

func main() {
var a int
var b int
//var c chan int= make(chan int)
//var c = make(chan int)
c := make(chan int)
go sigma(1, 100, c)
go sigma(101, 200, c)
fmt.Println("main")
a, b = <-c, <-c
fmt.Println(a + b)
}



실제로도 main에 먼저 도달한다.

그럼 정말로 a값의 상태는 어찌 되는가 하면 결과를 보면 알겠지만 제대로 계산된걸 알 수 있다.

그 이유에 대해서 설명해 주겠다.


다른언어에서는 이런 동기화를 해결하기 위해서 약간 거지같은 방법을 사용한다.

이를 언급하지는 않겠다. 우리는 go의 방법만 알면 충분하다.


우리는 동기화를 위해서 무슨 작업을 하지는 않았지만 사실 채널 자체가 동기화에 대한 방법중 하나이다.

위의 작업이 일어나는 순서, 그리고 그 때마다 무슨일이 일어나는지 알아보자.


1. a = <- c 에 도달한다. 그 순간 main은 멈춘다.(block된다.)

2. 조금 뒤 첫번째(매우 높은 확률로) sigma함수의 c <- sum에 도달한다. a = <- c가 이미 도달했으므로 main의 블럭은 풀리고 값이 넘어간다.

3. b = <- c 에 도달한다. 그 순간 main은 험춘다.(block된다.)

4. 조금 뒤 두번째(매우 높은 확률로) sigma함수의 c <- sum에 도달한다. b = <- c가 이미 도달했으므로 main의 블럭은 풀리고 값이 넘어간다.


중요한것은 block과 동기화이다.


채널 변수는 수신이건 송신이건 간에 block이 된다.

즉 화살표에 도달하는 순간 해당 고루틴은 정지하게 된다.


위의 경우도 main의 수신부에 먼저 도달해서 멈췄다. 그 다음 sigma의 송신부에서 도달에 성공했다. 그래서 멈춘걸 풀었다.

반대로 sigma의 송신부에 먼저 도착했다면 sigma가 멈추고 main의 수신부가 도달하면 멈춘걸 풀게 된다.

이 방식으로 go루틴은 수신과 송신, 둘중 하나가 먼저 도착하면 락이 걸린다.

반대가 다시 도달할때까지 무한정 기다리고 반대가 도달하면 락이 풀리고 이제 같이 진행하게 된다.


이제 고루틴이 어떻게 동기화를 유지하는지 알게 되었다.

다만 이 모델에서 한가지 문제점이 있다.

그 문제점과 해결방법에 대해서 다음장에 소개하도록 하자.


+ Recent posts