Coroutine과 Thread 비교

쓰리원·2022년 6월 6일
0

Coroutine 정리

목록 보기
1/4
post-thumbnail

1. 동시성과 병렬성(Concurrency & Parallelism)

1. Concurrency 동시성

Interleaving(시분할)로 다수의 Task들을 평등하게 조금씩 나누어 실행합니다.

총 실행시간은 Context Switching에 대한 비용을 제외하면 각 Task 수행시간을 합친것과 동일합니다. 위 그림과 같이 2개의 Task 각각이 10분씩 걸린다고 했을때, 총 20분이 소요 됩니다.

2. Parallelism 병렬성

Parallelize(병렬화)로 다수의 Task를 동시에 수행합니다.

Task의 수 만큼 CPU 프로세서 자원이 필요하며, Context Switching을 하지 않습니다. 총 실행시간은 Task들 중 가장 소요시간이 긴 Task 만큼 걸립니다. 예를 들어 3개의 Task 각각이 10, 11, 12분씩 걸린다면, 총 12분이 소요 됩니다.

2. Thread 와 Coroutine 비교

Thread, Coroutine는 동시성(Concurrency)과 시분할(Interleaving)을 보장하기 위한 기술입니다. 여러개의 Task를 동시에 수행할 때 Thread 는 각 Task에 해당하는 메모리 영역을 할당하는데, 여러 Task를 동시에 수행해야하기 때문에 OS 레벨에서 각 Task들을 얼만큼씩 분배하여 수행해야지 효율적일지 Preemptive(선제적) Scheduling을 필요로 합니다. A작업 조금 B작업 조금을 통해 최종적으로 A작업과 B작업 모두를 완료하는 것입니다.

Coroutine은 Lightweight Thread 라고 불립니다. 이 또한 Task를 효율적으로 분배하여 조금씩 수행하여 완수하는 Concurrency를 목표로하지만 각 Task에 대해 Thread 를 할당하는 것이 아니라 작은 Object만을 할당해주고 이 Object들을 자유자재로 스위칭함으로써 기존의 Switching 비용을 줄였습니다.

1. Thread

  • Task 단위 = Thread
  1. 다수의 작업 각각에 Thread를 할당합니다.
  2. 각 Thread 는 위에 설명했듯 자체 Stack 메모리 영역을 가지며 JVM Stack 영역을 차지합니다.
  • Context Switching
  1. OS Kernel에 의한 Context Switching을 통해 Concurrency를 보장합니다.
  2. Blocking: Task 1(Thread)이 Task 2(Thread)의 결과가 나오기까지 기다려야한다면 Task 1 Thread는 Blocking되어 그 시간동안 해당 자원을 사용하지 못합니다.

위 그림에서 작업들은 모두 Thread 단위인것을 알 수 있습니다. Thread A 에서 Task 1을 수행중에 Task 2가 필요할때 이를 비동기로 호출하게 됩니다. Task 1은 진행중이던 작업을 멈추고(Blocked) Task 2는 Thread B에서 수행되며 이때 CPU 가 연산을 위해 바라보는 메모리 영역을 Thread A 에서 Thread B로 전환하는 Context Switching 이 일어납니다.

Task 2가 완료되었을때 해당 결과값을 Task 1에 반환하게 되고, 동시에 수행할 Task 3과 Task 4는 각각 Thread C와 Thread D에 할당됩니다. 싱글 코어 CPU는 동시 연산이 불가능하므로 이때에도 OS Kernel 의 Preemptive Scheduling에 의해 각 Task 1, 3, 4 각각을 얼만큼 수행하고 멈추고 다음 작업을 수행할지 결정하여 그에 맞게 세 작업을 돌아가며 실행함으로써 Concurrency를 보장합니다.

2. Coroutine

  • Task 단위 = Object (Coroutine)
  1. 다수의 작업 각각에 Object 를 할당합니다.
  2. 이 Coroutine Object는 객체를 담는 JVM Heap에 적재됩니다.
  • Programmer Switching = No Context Switching
  1. 프로그래머의 코딩을 통해 Switching 시점을 마음대로 정함으로써 Concurrency 를 보장합니다.
  2. Suspend (Non-Blocking): Task 1(Object) 이 Task 2(Object) 의 결과가 나오기까지 기다려야한다면 Task 1 Object 는 Suspend되지만 Task 1 을 수행하던 Thread는 그대로 유효하기 때문에 Task 2도 Task 1과 동일한 Thread에서 실행될 수 있습니다.

작업의 단위는 Coroutine Object 이므로 Task 1 수행중에 비동기 Task 2가 발생하더라도 Task 1을 수행하던 같은 Thread 에서 Task 2를 수행할 수 있으며, 하나의 Thread 에서 다수의 Coroutine Object 들을 수행할 수도 있습니다. 위 그림에 따라 Task 1과 Task 2의 전환에 있어 단일 Thread A 위에서 Coroutine Object 객체들만 교체함으로써 이뤄지기 때문에 OS 레벨의 Context Switching 은 필요없습니다. 한 Thread 에 다수의 Coroutine 을 수행할 수 있음과 Context Switching 이 필요없기 떄문에 Coroutine 을 Lightweight Thread 로도 부릅니다.

다만 위 그림의 Thread A와 Thread C의 예처럼 다수의 스레드가 동시에 수행된다면 Concurrency 보장을 위해 두 Threads 간 Context Switching 은 수행되어야합니다. 따라서 Coroutine 을 사용할때에는 No Context Switching 이라는 장점을 최대한 활용하기 위해 다수의 Thread 를 사용하는 것보다 단일 Thread 에서 여러 Coroutine Object 들을 실행하는 것이 좋습니다.

3. 정리

Coroutine 으로 ‘작업’의 단위를 Thread가 아닌 Object로 축소하면서 작업의 전환 및 다수 작업 수행에 굳이 다수의 Thread를 필요로 하지 않게 됩니다.

Coroutine은 Thread의 대안이 아니라 기존의 Thread를 더 잘게 쪼개어 사용하기위한 개념입니다. 하나의 Thread가 다수의 코루틴을 수행할 수 있기 때문에 더 이상 작업의 수만큼 Thread를 양산하며 메모리를 소비할 필요가 없습니다.

각 스레드마다 갖는 Stack 메모리 영역을 갖지 않기때문에, 스레드 사용시 스레드 개수만큼 Stack 메모리에 따른 메모리 사용공간이 증가하지 않아도 됩니다. 같은 프로세스내에 ‘공유 데이터 구조’(Heap)에 대한 locking 걱정도 없습니다.

3. Thread 와 Coroutine 수행 Task

Thread 와 Coroutine 의 예로 보여드린 그림들을 위와 같이 축약해보았습니다. Coroutine 을 사용한다면 Task 가 바뀌어도 Thread 는 그대로 유지되는 것을 볼 수 있습니다.

그에 따라 자연스레 Context Switching 횟수도 확연히 줄어들은것을 볼 수 있습니다. Coroutine 에서 설명드린바와 같이 Task 3 과 Task 4 도 Thread C 가 아닌 Thread A 에서 수행되도록 한다면 하나의 Context Switching 도 없게 설계할 수 있습니다. 즉, Coroutine 이 수행될 Thread 도 프로그래머가 Shared Thread Pool 을 지정하여 결정한다는 의미가 됩니다.

4. reference

https://aaronryu.github.io/2019/05/27/coroutine-and-thread/

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글