티스토리 뷰

Android

[안드로이드/Android] Coroutine, Thread - 차이와 특징

장구치는 개발자 2020. 11. 15. 16:21


간결하고 직관적인 코드로 비동기 처리 및 스레드 전환이 가능한 Coroutine 을 즐겨 사용하고 있습니다. Coroutine 은 이런 간결성, 편의성 외에도 경량 Thread 라고도 불리며 적은 기존 Thread 사용 방식에 비해 적은 OverHead 를 발생시키는 것으로 소개됩니다.

그런데 늘 표현이 "경량 Thread 이다" 가 아니라 "경량 Thread 라고도 불린다" 라고 설명되더군요. 

그래서 Thread 라는건지, 아닌건지...

그래서 오늘은 이 둘의 차이와 특성을 정리해 보고자 합니다.

 

Process & Thread

Process: Program 이 메모리에 적재되어 실행되는 인스턴스
Thread: Process 내 실행되는 여러 흐름의 단위

먼저 Thread 는 Process 보다 작은 단위의 실행 인스턴스로만 알고 있는데, 메모리 영역도 조금 다릅니다.

Process 는 독립된 메모리 영역(Heap)을 할당받고 각 Thread도 독립된 메모리 영역(Stack)을 할당받습니다. Thread 는 본질적으로 Process 내에 속해있기 때문에 Head 메모리 영역은 해당 Process 에 속한 모든 Thread 들이 공유할 수 있습니다.

프로그램 에 대한 Process 가 생성되면 Heap 영역과 하나의 Thread 와 하나의 Stack 영역을 갖게되고, Thread 가 추가될때마다 그 수만큼의 Stack 이 추가됩니다. Thread 가 100 개라면 전체 메모리에 100 개의 Stask 이 생성되는 것입니다.


Thread & Coroutine

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

Thread는 각 작업에 대해 Thread 를 할당합니다. 하지만 Coroutine 을 사용하면 각 작업에 대해 Thread 를 할당하는 것이 아니라 작은 Object 만을 할당해주고 이 Object 들을 자유자재로 스위칭함으로써 Switching 비용을 최대한 줄였습니다.



Thread

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

위 그림에서 작업들은 모두 Thread 단위인것을 알 수 있습니다. Thread A 에서 작업 1을 수행중에 작업 2가 필요할때 이를 비동기로 호출하게 됩니다. 작업 1은 진행중이던 작업을 멈추고(Blocked) 작업 2는 Thread B 에서 수행되며 이때 CPU 가 연산을 위해 바라보는 메모리 영역을 Thread A 에서 Thread B 로 전환하는 Context Switching 이 일어납니다. 작업 2가 완료되었을때 해당 결과값을 작업 1에 반환하게 되고, 동시에 수행할 작업 3과 작업 4는 각각 Thread C 와 Thread D 에 할당됩니다. 싱글 코어 CPU 는 동시 연산이 불가능하므로 이때에도 OS Kernel 의 Preempting Scheduling 에 의해 각 작업 1, 3, 4 각각을 얼만큼 수행하고 멈추고 다음 작업을 수행할지 결정하여 그에 맞게 세 작업을 돌아가며 실행함으로써 Concurrency 를 보장합니다.




Coroutine

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



작업의 단위는 Coroutine Object 이므로 작업 1 수행중에 비동기 작업 2가 발생하더라도 작업 1을 수행하던 같은 Thread 에서 작업 2를 수행할 수 있으며, 하나의 Thread 에서 다수의 Coroutine Object 들을 수행할 수도 있습니다. 위 그림에 따라 작업 1과 작업 2의 전환에 있어 단일 Thread A 위에서 Coroutine Object 객체들만 교체함으로써 이뤄지기 때문에 OS 레벨의 Context Switching 은 필요없습니다.(동일 Coroutine Scope 내에 작업 1과 2를 선언했을 경우 입니다. 별도의 스코프를 발생시켜 작업1, 작업2 를 실행시킨다면 단일 Thread A는 보장되지 않습니다) 한 Thread 에 다수의 Coroutine 을 수행할 수 있음과 Context Switching 이 필요없기 떄문에 Coroutine 을 경량 Thread 로도 부릅니다.

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


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



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

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



댓글