쾌락코딩

코루틴을 이해하기 위한 발악 2편

|

틈틈히 코루틴 자료들을 찾아 보면서 내가 왜 코루틴을 이해하는데 이렇게 오래걸리는지에 대해 깨달은 점이 있다. 머릿속에서 나도 모르게 자꾸만 쓰레드가 떠오르기 때문이었다.

코루틴을 공부할 때는 쓰레드에 엮이면 안된다. 쓰레드에 대한 개념은 쿨하게 버리고 코루틴을 새로운 개념으로 공부해야 했다.

하지만 코루틴이라는 개념을 처음 접하는 입장에서 동시성(concurrency)을 이해하려면, 당연히 쓰레드부터 떠올라 쉬운 일이 아니다. 심지어는 쓰레드를 떠올리는게 잘못 된것이라는 생각조차 들지 않았다.

이제부터는, 코루틴 자료들을 보면 여전히 쓰레드 관련 이야기가 많이 나오겠지만, 코루틴 자체를 이해하기 위해 코루틴을 쓰레드로 대체해서 생각하거나 엮어서 생각하지 말자.

코루틴은 코루틴이다.

동시성(concurrency)과 병렬성(parallelism)

동시성(concurrency)은 병렬성(parallelism)이 아니다.

코루틴을 공부하기에 앞서 짚고 넘어갈 간단한 OS지식이다.

동시성이란 프로그램이 마치 여러가지 일을 동시에 하는 것 처럼 느껴지게 하기 위하여 여러 쓰레드를 아주 잘게 쪼개어 시분할을 하는 것이다.

쓰레드

thread-concurrency

운영체제의 쓰레드 스케쥴러(Thread Scheduler)는 한 번에 한가지 쓰레드만 처리할 수 있다. 만약 코드에서 쓰레드를 5개 생성했다고 가정하자. 각 쓰레드가 가지는 작업들 마다 걸리는 시간이 모두 다르다. 이 5개의 작업들을 마치 동시에 실행하는 것처럼 하기 위해 짧은 시간동안 쓰레드1을 실행, 그 다음 쓰레드를 바꿔 아주 짧은 시간동안 쓰레드 2를 실행, 같은 방법으로 쓰레드 3을 실행… 을 반복하고 다시 쓰레드 1을 반복한다. 사실 한 번에 한 개의 작업만을 하는 것은 다름 없으나 마치 동시에 실행되고 있는 것 같다.

위의 설명이 쓰레드를 사용한 동시성 프로그래밍이다. 언제 어떤 쓰레드를 몇 초 동안 실행시킬지, 그리고 이전 쓰레드를 멈추고나서 다음은 어떤 작업 쓰레드를 실행시킬지 등등을 모두 OS가 관리한다. 참고로 쓰레드를 이렇게 자주 변경하는 것은 많은 리소스가 필요한 무거운 작업이다.

한편 최근에는 하나의 CPU가 여러개의 core를 가지고 있다. B쓰레드가 CPU core2 에서 돌아가는 동안, A쓰레드는 CPU core1에서 돌아갈 수 있다. 정말 실제로 두 작업이 동시에 실행되는 것이다. 이게 병렬성이다.

동시성과 병렬성은 엄연히 다른 개념이다.

코루틴

코루틴의 동시성 프로그래밍을 보자. 코루틴-동시성

코루틴은 개념적으로 쓰레드와 비슷하다. 경량 쓰레드라고도 불린다.

나는 이말에 집착한 나머지 자꾸 쓰레드 거의 동일한 개념으로 생각하려 했기에 이해가 많이 늦었다. 코루틴을 경량 쓰레드라고 표현한 것은, 실제 쓰레드는 아니지만 결과적으로 목적이 쓰레드와 같은 반면 더 성능이 좋고 가볍기 때문에 붙여진 별명이라고 생각한다. 조금 더 심도 있게 들어가고싶다면 Difference between thread and coroutine in Kotlin 를 참고 하자.

경량쓰레드라니

코루틴 관련 자료들 대부분이 코루틴을 여러개 만들면서 동시성 프로그래밍을 선보이고 있다. 내가 알고 있던 동시성 프로그래밍 방법은 글 윗부분에서 설명한 쓰레드를 사용한 방법 뿐이었다. 무조건 그 방법밖에 없는줄 알았기 때문에 코루틴으로 동시성이 가능한 것을 보고는 코루틴도 결국 쓰레드들이라고 생각했었다. 그러나 코루틴은 쓰레드 위에서 돌아가긴 하더라도 특정 쓰레드에 종속적이지 않을 수 있다.

코루틴은 그저 하나의 쓰레드(혹은 스케줄러)위에서 실행이 시작될 수가 있다. 하나의 쓰레드에 코루틴이 여러개 존재할 수가 있는데, 실행중이던 하나의 코루틴이 suspend(멈춤)되면, 현재 쓰레드에서 resume(재개)할 다른 코루틴을 찾는다. 다른 쓰레드에서 찾는게 아니라 같은 쓰레드에서 찾는것이다(물론 추후에 다루겠지만 다른 쓰레드에서 resume할 수도 있다). 따라서 쓰레드를 switch하는데 드는 overhead가 없다.

또 하나, 위에서 말한 suspend, resume 등등을 모두 개발자가 직접 컨트롤 할 수 있다. 여러 작업을 가지고 동시성 프로그래밍을 할 때 모두 OS가 컨트롤 했던 쓰레드 방식과는 다르다. Thread 방식의 경우 Thread 캐치를 CPU idea 상태에 따라서 알아서 처리하는대신 이걸 개발자가 직접 처리할 수 있는게 코루틴이다. 이점이 꽤나 큰 장점이라고 한다.

헷갈릴수 있는것이, 코루틴의 경우 절차적 프로그래밍 처럼

method1()
method2()

순서로 코드를 작성했지만 실제 실행될 때 method2(), method1()순서로 실행될 수가 있다. 쓰레드 2개로 분리되어 순서가 변경된게 아니라, 정말 하나의 쓰레드에서 돌아가는데 실행 순서만 바뀌는 것이다(물론 개발자 의도 하에). 컴파일 될때 컴파일러가 그런 순서로 동작 될수 있는 구조가 되도록 코드가 추가된다고 보면 될 것 같다.

하나의 코루틴 멈춤(suspend)에 대한 간단한 예시

코틀린 웹 서버 프레임워크인 Ktor에는 http 요청 함수인 HttpClient.post가 있다. 이 함수는 결과 값을 반환하기 전에 결과를 기다릴 시간이 필요한 함수이다. 즉 잠시 멈추었다가 결과값이 네트워크를 타고 돌아오면 그떄서야 실행해야할 함수, suspend(일시적으로 멈출) 함수인 것이다.

그래서 간단히 suspend 키워드로 표시된다.

suspend fun getReturnValueFromServer(): SomeType {
 ...
}

즉 코드가 suspend function을 호출하는 순간 해당 코루틴을 잠시 중단 시켜놓을 수 있으며 결과값이 왔을 때 이 함수를 다시 resume할 수 있다. 또한 함수가 suspend되었을 시점에 즉시 다른 resume가능한 코루틴을 찾아 그 코루틴을 실행하다가, getReturnValueFromServer()가 결과값을 들고 나타나면 다시 resume할 수 있다.

마무리

코루틴은 동시성 뿐만 아니라 병렬 실행 역시 가능하게 한다. 또한 메인 쓰레드에서만 쓰레드를 돌리는게 아니라 네트워크 요청같은 경우 다른 쓰레드를 사용하기도 하는데, 이러한 내용들은 코루틴을 이해한 다음에 +alpha가 되야하는 부분이라서 다루지 않았다. 어느정도 코루틴에 대한 이해와 감이 잡혔으니 다음 발악때는 조금더 깊고 넓은 사용법을 알아봐야겠다.

Comments