728x90

동시성 프로그래밍이란 여러가지 작업(태스크:task)가 동시에 처리되는 것을 의미한다.

병렬 프로그래밍이랑 비슷하긴한데 사실 같은 개념은 아니다.

사실 이를 진지하게 논의할 시간은 아닌것 같고 동시성(concurrency)이란 것은 라운드로빈, 시분할처리를 생각하면 편하다.

병렬성(parallelism)은 정말로 동시에 같이 일어나는 것을 의미한다. 멀티프로세싱이나 멀티쓰레딩을 생각하면 된다.


갑자기 이런 이야기를 하는것은 앞으로 설명할 고루틴 때문이다.


갑자기 운영체제를 하자면 컴퓨터는 여러작업을 동시(concurrency)에 처리하기 위한 작업이 필요하다.

여러분이 음악을 들으면서 타자를 치거나 게임을 하면서 알람앱을 키는것도 모두 동시성 프로그래밍의 증거이다.

이렇게 동시성을 구현하기 위해서 사용하는 개념이 바로 멀티프로세싱과 멀티쓰레딩이다.


프로세스와 쓰레드의 차이는 뭐 중요하지 않으므로 제쳐두겠다.(여기에서 중요하지 않다는 거지 둘은 매우 다르다.)

중요한건 프로세스와 쓰레드는 모두 운영체제에서 관리한다는 것이다.

흔히 프로그래밍을 할때 프로세스를 포크하거나 쓰레드를 생성하는 작업은 사실 운영체제의 동의 하에서 이루어진다.

운영체제가 거부한다? 그러면 불가능한거다. 운영체제가 지원안한다? 그것도 불가능 한거다.


하지만 여러분은 운영체제라는 말을 들었을 때 느낌이 와야한다.

프로세스와 쓰레드는 운영체제를 거치기 때문에 필연적으로 오버헤드(전처리)가 생긴다.

프로세스는 정말 많은 오버헤드가 발생하고 쓰레드는 프로세스에 비해 오버헤드가 적게 발생한다.

하지만 쓰레드의 오버헤드도 프로세스에 비해서 많은 거지 사실 무작정 열기에는 오버헤드가 크다.


프로세스와 쓰레드를 같이 말하는 이유는 간단한데 운영체제는 쓰레드 단위로 스케줄링하기 때문이다.

여러분이 음악을 들으면서 게임을 하는걸 가능케 하기위해서 운영체제는 프로세스와 쓰레드를 시분할로 쪼개서 라운드로빈하게 돌린다.

그러면 프로세스와 쓰레드는 서로 교체가된다. 한번은 음악, 한번은 게임, 이런식으로 반복하면서 실행한다.

이런걸 컨텍스트 스위치(context switch)라고 부른다. 이 작업도 만만치 않다. 프로세스가 더 시간걸리고 쓰레드가 좀 덜걸린다.

하지만 쓰레드의 컨텍스트 스위칭은 경우에 따라서 부담될 수 있다. 잦게 쓰레드를 생성한다면 말이다.


결론부터 말하자면 프로세스와 쓰레드는 os의 스케줄링에 따라서 움직이고 생성,삭제,컨텍스트 스위치에서 오버헤드가 크다는 것이다.


결국 go에서는 그럼 os에서 스케줄링안하고 내가(go runtime) 스케줄링할께!하고 만든것이 바로 goroutine이다.

그래서 go에서는 쓰레드를 쓰지 않는다. goroutine을 쓸 뿐이다.

더 정확히 말하면 쓰레드와 프로세스를 os에게 내놔라고 하지 않고 그냥 goroutine을 돌리면 알아서 필요할 경우 쓰레드를 꺼내서 쓴다.(go 1.5버전 이후)

일반적으로는 go runtime에서 자체적으로 시분할 처리를 해서 사용한다.


잡설이 길었다. 중요한 내용이다보니 설명이 길어졌다.


Process - 공식적인 설명은 운영체제에서 사용하는 task의 단위이나 쉽게 말하면 main함수라고도 할 수 있다. 프로그램을 실행시키면 가장 먼저 켜진다. 프로세스를 켜기 위해서는 os에게 요청을 해야한다. 프로세스는 또다른 프로세스를 켤 수 있다. 메모리 영역은 독립적이고 서로 침범할 수 없다. 그래서 서로 IPC를 사용해서 통신의 개념으로 데이터를 주고 받는다. 생성과 삭제, 컨텍스트 스위칭시에 비용이 크다.


Thread - 모든 프로세스는 반드시 적어도 한개는 쓰레드를 가진다. 쓰레드는 프로세스를 다시 단위로 쪼갠 것이다. 쓰레드를 생성하기 위해서는 os에게 요청을 해야한다. 쓰레드는 다른 쓰레드를 실행할 수 있다. 모든 쓰레드가 죽어야 프로세스가 죽는다. 메모리 영역은 독립적인 부분(스택)을 제외하곤 나머지 부분은 같은 프로세스라면 공유한다. 생성과 삭제, 컨텍스트 스위칭시의 비용이 프로세스보다는 작다.


Goroutine - os에게 요청하지 않고 사용하는 경량 쓰레드. 흔히 경량 쓰레드라고 부르지만 실제로는 os요청이 없으므로 쓰레드라고 부르긴 좀 애매한녀석이다. os에서 실행하지 않는다는 말은 바꿔말하면 go언어 내부에서 자체적으로 관리하고 실행한다는 뜻이다. 내부에서는 시분할 처리를 하기에 동시성은 만족하지만 병렬성은 만족할지안할지는 알 수 없다. 다만 병렬성을 만족하게 의도할 수는 있다. 당연히 os에 대한 요청이 없으므로 비용이 프로세스보다도 작다.


package main

import (
"fmt"
"time"
)

var count int

func sub() {
for i := 0; i < 50; i++ {
count++
time.Sleep(time.Second / 10)
fmt.Printf("sub: %d\n", count)
}
}

func main() {
go sub()
for i := 0; i < 50; i++ {
count++
time.Sleep(time.Second / 10)
fmt.Printf("main: %d\n", count)
}
}

goroutine을 사용하는 코드이다. 눈에 띄는 것을 go라는 키워드이다.


go sub()

기존에 더럽게 선언하던 따른 언어들(얼마나 드러운지 보고싶으면 C를 봐라. JAVA도 저것보단 복잡하게 호출한다.)에 매우 간결하다.

go키워드를 선언만 해주면 자동으로 goroutine으로 실행해서 동시성프로그래밍이 작동된다.


과연 동시성 프로그래밍이 제대로 작동하고 있을까?

궁금하다면 동작시켜보면 그만이다.


보면 main과 sub가 동시에 실행되고 있다.


고루틴은 간단하게 동시성 프로그래밍을 구현하지만 모든 동시성 프로그래밍이 가지는 숙명인 동기화 문제가 남아있다.

고루틴이 동기화 문제를 어떻게 해결하는지는 다음 장의 채널에서 설명하도록 하겠다.


+ Recent posts