loading
본문 바로가기
Language/go lang.

[Go] 고루틴(go-routine)

by 개발자 김모씨 2020. 9. 13.
반응형

 

앞서 Go언어 입문에 포스팅하였듯,
Go언어의 대표적 특징 중 하나에는 병행성이 있다.

Go언어는 'go' 키워드를 사용하여, 고루틴을 만드는데,
여기서 고루틴은 비동기적(asynchronously)으로 특정 함수 루틴을 실행하므로,
여러 함수(또는 코드)가 동시에(Concurrently) 실행된다.(병행성의 만족)

 

병행성(Concurrency)

흔히들 병렬성(Parallelism)과 병행성(Concurrency)을 혼동하곤 한다.
병렬성은, 실제로 여러 작업을 동시에 수행하는 것을 의미한다.
병행성은, 마치 여러 일을 동시에 하듯이 수행하는 것을 의미한다.
쉬운 예로 둘
을 비교해보자.

이 글을 적고 있는 필자는 현재, '커피를 마시면서' '포스팅'을 하고 있다.
하지만 여기서, 커피를 마시는 일과 포스팅을 하는 일이 '물리적으로' 동시에 진행되고 있는가?
그렇지 않다.
키보드를 두들기다가, 타이핑을 잠시 멈추고, 커피를 몇 모금 마시고, 타이핑을 계속하는
흐름이 N회 반복된다.
다시 말하자면, 이는 '논리적으로' 동시에 진행되는 일처럼 보이지만, '물리적으로' 동시에 진행되는 일은 아니다.

정리해보자.

<병행성 vs. 병렬성>

병렬성(Parallelelism)이란, 실제로 동시에 작업이 처리되는 것이라고 하였다.
그에 따라, 병렬성은 Physical(Machine) 레벨에 속한다.
Computer 영역으로 들어오면, 이는 Multi Core 환경에서만 실제 동시 수행이 가능하다.
각기 다른 CPU 또는 GPU Core가 서로 다른 프로그램의 기능을 수행하면서, 각 프로그램들이 같은 순간에 실행된다.

병행성(Concurrency)이란, '동시에 진행하듯이' 처리되는 것이라고 하였다.
그에 따라, 병행성은 Logical 레벨에 속한다.
다시 말하면, 병행성은 병렬성보다 더 넓은 개념이다.(포괄)
Computer 영역으로 들어오면,
Single Core의 경우, Thread 간의 Time-Sharing(시분할 : 매우 작은 시간 단위로 여러 thread를 순차적으로 반복 실행함으로써 마치 동시에 실행되는 것처럼 보이게 함) 기법을 통해, 병행성을 만족할 수 있다.
Multi Core의 경우, 실제 물리적으로 병행성이 만족된다.

위의 '커피를 마시면서 포스팅'을 하는' 상황을 다시 설명하자면,
병행성은 만족하나 병렬성은 만족하지 않는다 라고 정리할 수 있겠다.

 

병행성의 난제 : 비용의 증가

좀 더 컴퓨팅 영역 안으로 들어가보자.

 

CPU의 Time Sharing


앞서 Time-Sharing의 이야기를 하였는데,
Process 또는 Thread를 OS가 변경해주는 것을 컨텍스트 스위치(Context Switch)라고 한다.
(Process와 Thread의 차이는 여기서 핵심이 아니므로 넘어가겠다)
Context Switch는 OS가 현재 실행 중인 Process 또는 Thread의 상태를 임의 공간에 저장하고,
새로 실행할 Process 또는 Thread의 상태를 로드하는 행위 등을 말한다. (실제로는 보다 복잡하다)
실제로, 우리가 PC를 사용할 때 멜론 등을 통해 '음악을 들으면서' '게임을 하는' 등의 모든 동작에서
Context Switch는 매우 빈번하게 발생한다.

문제는 OS의 스케쥴링에 따라 발생하는 이 컨텍스트 스위치의 오버헤드가 크다는 것에 있다.
Multi Thread를 사용하는 대표적인 언어인 C++, JAVA 등에서 이를 구현해본 독자들은 잘 알텐데,
Thread를 많이 사용할수록 CPU 이용률, 메모리 사용량 등은 매우 급격하게 증가한다.
또한 Context Switching은 프로그램의 영역이 아닌, OS의 영역이기 때문에,
Thread가 많아질수록 프로그램의 Job 수행 시간을 예측할 수 없게 된다.

이러한 비용의 증가와 복잡도의 증가는
개발자들에게 있어 숙명이며 고질적인 스트레스라고 할 수 있다.

 

고루틴(Go Routine) : 프로그램에서의 Time-Sharing 처리

<가장 빨리 만나는 Go 언어 발췌> : 고루틴이 스레드보다 더 작은 개념이다.

 

고루틴의 특징은
'OS가 아닌 프로그램에서의 Time-Sharing 처리'가 핵심이다.

앞서 말한 것처럼, Thread는 OS의 커널 단에서 제공하는 리소스 이기 때문에,
많이 생성하고 자주 바뀔수록 오버헤드 등의 비용적 부담이 매우 커진다.

그래서 Go 언어에서는 Thread를 사용하지 않는다. 고루틴(Go Routine)을 사용한다.
이를 쉽게 표현하자면,
Go 언어: "OS! Thread 때문에 많이 힘들지? 내가 알아서 스케쥴링 하고 관리할게!"
하는 느낌이랄까...
GO 언어는 OS에 Process나 Thread를 요청하지 않고, 알아서 자체적인 Time-Sharing을 통해 처리한다.
그래서 비용이 OS에 의한 Process, Thread의 Context Switching에 비해 현저히 낮다.

또한, 고루틴은 Thread에 종속적이지 않기 때문에,
1개의 Thread 에서 1개 이상의 고루틴이 Time-Sharing 처리되어 사용될 수 있다.

Time-Sharing이기 때문에,
당연히 병행성(Concureency)은 만족하지만,
병렬성(Parallelism)의 만족 여부는 개발자의 구현에 따라 다르다.

 

고루틴(Go Routine)의 구현

우리가 다른 언어에서 봐왔던 Multi Thread는 구현이 매우 어렵다.
정말이지, 매우 더럽고 어렵다...... (규모가 큰 프로젝트를 경험해 본 개발자들은 뼈저리게 공감할 것이다)
(참고: C++ 멀티 스레드에 관한 개발자의 고찰 - elky84.github.io/2013/04/09/consideration_multithread_in_cpp/)

 

C++ 멀티 스레드 프로그래밍을 몇년간 해온 후 느낀 고찰 · Elky Essay

서버 프로그래머가 되기 이전엔 멀티 스레드 따위 관심도 없었다. 물론 그 시기까지가 클럭 향상 -> 멀티 코어로 변화가 이루어지기 전이기도 했지만… 여하튼 나는 그런 것 보단 다른 것들에 관

elky84.github.io

 

//C

#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
 
DWORD WINAPI ThreadFunc(LPVOID);
 
int global = 1;
 
int main() {
    HANDLE hThrd;
    DWORD threadId; 
    int i;
    
    for (i=0; i<5; i++) {
        //CreateThread함수를 5번 호출
        hThrd = CreateThread(NULL,
            		     0, 
            		     ThreadFunc, //함수포인터
            		     (LPVOID)i,
            		     0,
            		     &threadId);
                        
        if (hThrd) {
            printf("Thread launched %d\n", i);
            CloseHandle(hThrd);
        }
    }
    
   Sleep(1000);  // Wait for the threads
 
    return 0;
}
 
DWORD WINAPI ThreadFunc(LPVOID n) {
    int i;
    for (i=0;i<100;i++) {
        printf("%d%d%d%d%d%d%d%d global = %d\n",n,n,n,n,n,n,n,n,global++);
    }
    return 0;
}

C언어를 사용해 간단한 thread를 구현해보았다.
여기까지는 그나마 작성이 어렵지 않은 수준이지만,
여기서 Thead간의 Synchronous를 위한 기능이 추가된다면 상황이 많이 복잡해진다....
(Shared Memory 등을 통해 수시로 특정 값을 확인하거나, Message를 통해 Event를 발생시켜야 한다)

반면.....

그렇다면, 우리의 Go 언어에서는 이를 어떻게 접근하였을까?
우리의 Go 언어는 이를 'go' 라는 키워드(!) 단 하나로 정리하였다.

//golang

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func hello(n int) {
	r := rand.Intn(100)          // 랜덤한 숫자 생성
	time.Sleep(time.Duration(r)) // 랜덤한 시간 동안 대기
	fmt.Println(n)               // n 출력
}

func main() {
	for i := 0; i < 100; i++ { // 100번 반복하여
		go hello(i)        // 고루틴 100개 생성
	}

	fmt.Scanln()
}

100개의 고루틴을 생성한 예제이다.
main() 함수 에서 볼 수 있듯, "'go' 키워드 + 함수명" 을 통해 매우 간단하게 고루틴을 생성하였다.
일반적인 Thread와 마찬가지로, 해당 함수 종료 시 고루틴은 자동 종료된다.

 

이처럼 고루틴은 다른 언어의 Thread 생성 방법보다 문법이 간단하고,
Thread 보다 운영체제의 리소스를 적게 사용하므로,
부담 없이 많은 수의 고루틴을 쉽게 생성할 수 있다.

물론, Multi-Thread 환경과 마찬가지로,
Go의 고루틴도 고루틴 간의 Synchronous가 매우 중요하다.
Go는 이 역시 채널(Channel)을 통해 매우 간단하고 혁신적으로 처리하였는데,
이와 관련해서는 다음 포스팅에서 알아보자.

 

<찬양하라 (갓)구글>

반응형

댓글