조각들

단일 스레드의 한계

멀티 스레드 프로그래밍이 필요한 이유
병렬 처리
사용자 스레드, 데몬 스레드   
Executor

Executor 프레임웍은 작업 처리를 위해 스레드풀을 미리 생성해 놓고 작업을 요청 받으면 쉬고 있는 스레드에 작업을 분배한다. 이때 각 스레드가 작업을 끝내더라도 스레드를 종료하지 않고 다음 작업이 들어오면 재사용한다. 이를 통해 스레드풀에 속한 스레드의 생성과 관리 및 작업 분배에 대한 책임을 Executor 프레임웍이 담당하므로 개발자는 더 이상 스레드를 직접 다루거나 관리하지 않아도 된다. 개발자가 해야 할 일은 스레드풀에 속한 스레드의 개수를 설정하고, 해당 스레드풀을 관리하는 서비스에 작업을 제출하는 것 뿐이다.

 

개발자가 ExecutorService 객체 내부의 동작을 전혀 신경 쓰지 않아도 된다는 것이다. 개발자는 스레드풀을 구성할 스레드의 ㄱ수를 지정하고 ExecutorService 객체에 작업을 제출하기만 하면 된다. 작업 대기열에 작업을 적재하고 스레드로 분배하는 일은 ExecutorService 객체가 한다.

 

하지만 Executor 프레임웍 또한 여러가지 문제가 존재하는데 그중 대표적인 문제가 바로 스레드 블로킹이다.

 

스레드 블로킹이란 스레드가 아무것도 하지 못하고 사용될 수 없는 상태에 있는 것을 뜻한다.

 

ExecutorService 객체에 제출한 작업에서 결과를 전달받을 때는 Future 객체를 사용해야 한다. Future 객체는 미래에 언제 올지 모르는 값을 기다리는 함수인 get 함수를 갖고 있고 get 함수를 호출하면 get 함수를 호출한 스레드가 결괏값이 반환될 때까지 블로킹된다.

 

스레드 블로킹을 일으키는 대표적인 함수는 바로 Thread.sleep이다. 이 함수는 명령받은 시간 동안 해당 스레드를 블로킹시킨다.

 

코루틴은 작업 단위로서의 코루틴이 스레드를 사용하지 않을 때 스레드 사용 권한을 양보하는 방식으로 스레드 사용을 최적화하고 스레드가 블로킹되는 상황을 방지한다.

 

CoroutineDispatcher 객체는 코루틴을 스레드로 보낸다. 코루틴은 일시 중단이 가능한 작업이기 때문에 스레드가 있어야 실행될 수 있다.

CoroutineDispatcher 객체는 자신에게 실행 요청된 코루틴을 우선 작업 대기열에 적재한 후 사용할 수 있는 스레드가 생기면 스레드로 보내는 방식으로 동작한다.

 

코루틴 라이브러리는 개발자가 직접 CoroutineDispatcher 객체를 생성하는 문제의 방지를 위해 미리 정의된 CoroutineDispatcher의 목록을 제공한다.

 

106pg

Dispatchers.IO와 Dispatchers.Default는 같은 스레드풀을 사용한다.

이유는 이 두가지가 코루틴 라이브러리의 공유 스레드풀을 사용하기 때문이다. 

 


스레드 블로킹
Future, get()
Thread.sleep
코루틴은 자신이 스레드를 사용하지 않을 때 스레드 사용 권한을 반납한다. 
 
runBlocking 함수는 해당 함수를 호출한 스레드를 사용해 실행되는 코루틴을 만들어 낸다.
람다식 내부의 모든 코드가 실행 완료될 때까지 코루틴은 종료되지 않는다.
runBlocking 함수로 생성된 코루틴은 실행 완료될 때까지 이 코루틴과 관련 없는 다른 작업이 스레드를 점유하지 못하게 막는다.
 
82pg 코루틴 이름 달기
90pg 디스패쳐 종류
 
자식 코루틴에 Coroutine Dispatcher 객체가 설정되지 않았으면 부모 코루틴의 CoroutineDispatcher 객체를 사용한다.
-> 같은 스레드풀 공유
 
개발자가 매번 Dispatcher를 직접 만들게 하기엔 비효율적일 가능성이 높아서 미리 세팅해둔 것 제공
-> IO, Default, Main
 
코루틴은 애플리케이션 레벨의 공유 스레드풀을 제공한다.
Dispatchers.Default.limitedParallelism은 공유 스레드풀 내 Dispatchers.Default의 스레드를 쪼개 쓰지만
Dispatchers.IO.limitedParalleism은 공유 스레드풀 내 새로운 스레드풀을 만들어낸다.
 -> Q. 이유가 있나?


Thread.sleep 함수는 해당 함수가 실행되는 동안 스레드가 블로킹돼 사용할 수 없는 상태가 되지만 delay 함수를 사용하면 해당 함수가 실행되는 동안 스레드는 다른 코루틴이 사용할 수 있다.
 
join 함수는 join 함수를 호출한 코루틴을 제외하고 이미 실행 중인 다른 코루틴을 일시 중단하지 않는다. 
launch 함수 호출 시 생성되는 코루틴은 코루틴을 실행시킬 스레드가 있다면 지연 없이 곧바로 실행된다.
 
코루틴을 지연 시작하기 위해서는 launch 함수의 start 인자로 CoroutineStart.LAZY를 넘겨 코루틴에 지연 시작 옵션을 적용해야 한다. 
지연 코루틴을 실행하기 위해서는 Job 객체의 start 함수를 명시적으로 호출해야 한다. 
-> Q. 코루틴을 지연 시작해야 하는 경우가 있나? 


코루틴이 실행될 필요가 없어졌음에도 취소하지 않고 계속해서 실행되도록 두면 코루틴은 계속해서 스레드를 사용하며 이는 애플리케이션의 성능 저하로 이어진다.
 
cancel 함수를 사용하면, cancel의 대상이 된 Job 객체는 곧바로 취소되는 것이 아니라 미래의 어느 시점에 취소된다. 
취소에 대한 순차성 보장을 위해 Job 객체는 cancelAndJoin 함수를 제공한다.
*취소될 때까지 코루틴을 일시 중단시켜서 기다리게 하는 것일 뿐 이것 역시도 즉시 취소시키는 것은 아니다.
 
1. delay를 사용한 취소 확인
2. yield를 사용한 취소 확인
3. CoroutineScope.isActive를 사용한 취소 확인
를 사용하면 취소 확인 시점을 만들어 취소 요청 시 취소되도록 만들 수 있다. 
*취소 확인 시점이 없으면 취소를 해도 취소되지 않는다.
 
코루틴의 상태 6가지
생성, 실행중, 실행 완료 중, 실행 완료, 취소 중, 취소 완료
 
코루틴의 상태 3가지
isActive, isCancelled, isCompleted
 
145pg에 정리 표 있음 
 
launch-job
async-deferred
 
Deffered 객체는 결괏값 수신의 대기를 위해 await 함수를 제공한다.
코루틴이 실행 완료될 때까지 await 함수를 호출한 코루틴을 일시 중단하며, 실행 완료되면 결괏값을 반환하고 호출부의 코루틴을 재개한다.
 
Deffered는 Job의 서브타입이다.
 
awaitAll은 확장함수로도 제공된다.
 
withContext 함수를 사용하면 async-await 작업을 대체할 수 있다.
 -> Job과 Deferred의 차이가 뭐야?


164pg
withContext 함수는 겉보기에는 async와 await를 연속적으로 호출하는 것과 비슷하게 동작하지만 내부적으로 보면 다르게 동작한다. async-await 쌍은 새로운 코루틴을 생성해 작업을 처리하지만 withContext 함수는 실행 중이던 코루틴을 그대로 유지한 채 코루틴의 실행 환경(CoroutineContext(Dispatchers))만 바꿔서 실행된다. 
 
withContext 함수가 호출되면 실행 중인 코루틴의 실행 환경이 withContext 함수의 context 인자 값으로 변경돼 실행되며, 이를 컨텍스트 스위칭이라고 부른다. 
 ->
withContext 사용 시 주의점
withContext 함수는 새로운 코루틴을 만들지 않기 때문에 하나의 코루틴에서 withContext 함수가 여러 번 호출되면 순차적으로 실행된다. 즉, 복수의 독립적인 작업이 병렬로 실행돼야 하는 상황에 withContext를 사용할 경우 성능에 문제를 일으킬 수 있다.
 
같은 상황에서 위 문제를 해결하려면 withContext를 제거하고 코루틴을 생성하는 async-await 쌍으로 대체해야 한다. 이때 단순히 대체만하는 것이 아니라 Deffered 객체에 대한 await 함수 호출을 모든 코루틴이 실행된 뒤에 해야 한다. 
 
CoroutineContext의 구성 요소
CoroutineName: 코루틴의 이름을 설정한다.
CoroutineDispatcher: 코루틴을 스레드에 할당해 실행한다.
Job: 코루틴의 추상체로 코루틴을 조작하는 데 사용된다.
CoroutineExceptionHandler: 코루틴에서 발생한 예외를 처리한다.
 
CoroutineContext 객체는 키-값 쌍으로 각 구성 요소를 관리하며 각 구성 요소별 객체를 한 개씩만 가질 수 있다.
그리고 객체 간 더하기 연산자를 활용해 구성한다. 
 
val combinedCoroutineContext = coroutineContext1 + coroutineContext2
coroutineContext1의 구성 요소들은 coroutineContext2의 구성 요소들에 의해 덮어씌워진다.
 
CoroutineContext 객체의 구성 요소에 접근하기 위해서는 각 구성 요소가 가진 고유한 키가 필요하다.
CoroutineContext 객체는 구성 요소를 제거하기 위한 minusKey 함수를 제공한다.
 
minusKey 함수 사용 시 주의할 점

minusKey를 호출한 CoroutineContext 객체는 그대로 유지되고, 구성 요소가 제거된 새로운 CoroutineContext 객체가 반환된다는 것이다. 
 
193pg
구조화된 동시성(Structured Concurrency)의 원칙이란 비동기 작업을 구조화함으로써 비동기 프로그래밍을 보다 안정적이고 예측할 수 있게 만드는 원칙이다. 코루틴은 구조화된 동시성의 원칙을 사용해 비동기 작업인 코루틴을 부모-자식 관계로 구조화함으로써 코루틴이 보다 안전하게 관리되고 제어될 수 있도록 한다. 코루틴을 부모-자식 관계로 구좌화하는 방법은 간단하다. 부모 코루틴을 만드는 코루틴 빌더의 람다식 속에서 새로운 코루틴 빌더를 호출하면 된다.
 
구조화된 코루틴은 여러 특징을 갖는데 그 대표적인 특징은 다음과 같다.
- 부모 코루틴의 실행 환경이 자식 코루틴에게 상속된다.
- 작업을 제어하는 데 사용된다.
- 부모 코루틴이 취소되면 자식 코루틴도 취소된다.
- 부모 코루틴은 자식 코루틴이 완료될 때까지 대기한다.
- CoroutineScope를 사용해 코루틴이 실행되는 범위를 제한할 수 있다.
-> Q. 대충은 아는데 CoroutineScope라는 걸 한 문장으로 설명해보라고 하면 못하겠음 


부모 코루틴의 모든 실행 환경이 항상 자식 코루틴에게 상속되지는 않는다.
-> 자식 코루틴에 새로운 CoroutineContext 객체가 전달되면 덮어씌워진다.
 
*주의할 점은 다른 CoroutineContext 구성 요소들과는 다르게 Job 객체는 상속되지 않고 코루틴 빌더 함수가 호출되면 새롭게 생성된다는 것이다.
 
코루틴 제어에 Job 객체가 필요한데 Job 객체를 부모 코루틴으로부터 상속받게 되면 개별 코루틴의 제어가 어려워지기 때문이다.
Job의 동일성 여부는 컨텍스트 객체가 Job을 Key 값으로 전달하여 얻은 값들을 === 연산자를 활용하여 비교할 수 있다.
 
그런데 그렇다고 또 부모 코루틴의 Job 객체가 자식 코루틴의 Job 객체와 아무 관계도 없는 것은 아니다. 
 
자식 코루틴의 Job 객체는 parent 프로퍼티를 통해 부모 코루틴의 Job 객체에 대한 참조를 갖고, 부모 코루틴의 Job 객체 또한 children 프로퍼티를 통해 자식 코루틴의 Job 객체에 대한 참조를 갖는 것을 확인할 수 있다.
 
코루틴을 구조화하는 가장 중요한 이유는 코루틴을 안전하게 관리하고 제어하기 위함이다.
 
1. 코루틴으로 취소가 요청되면 자식 코루틴으로 전파된다. 부모 코루틴으로는 취소가 전파되지 않는다.
2. 부모 코루틴은 모든 자식 코루틴이 실행 완료돼야 완료될 수 있다.
 
코루틴은 자식 코루틴으로 취소를 전파하는 특성을 갖기 때문에 특정 코루틴이 취소되면 하위의 모든 코루틴이 취소된다.
 
invokeOnCompletion 함수 : 코루틴이 실행 완료되거나 취소 완료됐을 때 실행되는 콜백을 등록하는 함수
 
부모 코루틴은 마지막 코드를 실행한 시점부터 자식 코루틴의 실행 완료를 기다릴 때까지 '실행 완료 중'이라는 상태를 가진다.
 
'실행 중'과 '실행 완료 중'은 완전히 같은 Job 상태 값을 가져서 구분되지 않지만 코루틴의 실행 흐름을 이해하기 위해서는 자식 코루틴이 실행 완료되지 않으면 부모 코루틴도 실행 완료될 수 없다는 점을 이해하는 것이 중요하다.
 
CoroutineScope 객체는 자신의 범위 내에서 생성된 코루틴들에게 실행 환경을 제공하고, 이들의 실행 범위를 관리하는 역할을 한다. CoroutineScope도 커스텀하게 만들 수 있다. 
 
코루틴 빌더의 람다식은 CoroutineScope 객체를 수신 객체로 가진다. 이를 통해 코루틴의 실행 환경이 상속된다.
자식 코루틴에 실행 환경이 상속될 수 있었던 이유 또한 이 CoroutineScope 객체로부터 부모 코루틴의 실행 환경을 상속받았기 때문이다.
 
특정 코루틴만 기존에 존재하던 CoroutineScope 객체의 범우에서 벗어나게 만들려면 새로운 CoroutineScope 객체를 생성하고, 이 CoroutineScope 객체를 사용해 코루틴을 실행하면 된다. (227pg)
 
CoroutineScope 객체에 cancel 함수가 호출되면 범위에서 실행 중인 모든 코루틴에 취소가 요청된다. 
CoroutineScope 객체에 cancel 함수가 호출되면 CoroutineScope 객체는 자신의 coroutineContext 프로퍼티를 통해 Job 객체에 접근한 후 cancel 함수를 호출한다. 
 
같은 맥락으로 isActive도 coroutineContext에 설정된 Job 객체의 isActive 프로퍼티를 확인한다.
 
CoroutineScope 함수를 통해 CoroutineScope 객체가 생성되면 새로운 루트 Job이 생성되며, 이를 사용해 코루틴의 구조화를 깰 수 있다.
 
237pg 중요해보임.
 
코루틴의 구조화를 깬 후 delay 함수 등을 통해 구조화가 깨진 코루틴이 실행 완료되는 것을 기다리는 것은 코드를 불안정하게 만들기 때문에 실제 애플리케이션을 만들 때는 지양돼야 한다. 
 
Job()을 통해 Job 객체를 생성하면 그 자체로 루트 Job이 된다.
-> 루트 Job을 취소하면 하위의 모든 코루틴이 취소된다.
 
특정 Coroutine의 계층 구조만 끊으려면 새로운 Job 객체를 인자로 넘기면 된다.
244pg 그림 참고
 
또는 Job 생성 함수의 인자로 부모 코루틴의 Job 객체를 넘기면 된다.
 
launch 함수를 통해 생성된 Job 객체는 더 이상 실행할 코드가 없고, 모든 자식 코루틴들이 실행 완료되면 자동으로 실행 완료된다. 하지만 Job 생성 함수를 통해 생성된 Job 객체는 자동으로 실행 완료된다. 하지만 Job 생성 함수를 통해 생성된 Job 객체는 자식 코루틴들이 모두 실행 완료되더라도 자동으로 실행 완료되지 않으며, 명시적으로 완료 함수인 complete을 호출해야 완료된다.
 
runBlocking<Unit>(Dispatchers.IO){}
runBlocking 함수를 호출한 스레드는 메인 스레드이지만 runBlocking 코루틴은 Dispatchers.IO를 사용해 백그라운드 스레드에서 실행된다.
 
runBlocking 함수가 호출된 스레드와 다른 스레드에서 runBlocking 코루틴이 실행되더라도 해당 코루틴이 실행되는 동안 runBlocking 함수를 호출한 스레드는 차단된다. 
-> Dispatchers.IO에서 돌려도 일단 메인 스레드에서 호출했으니 메인 스레드는 차단된다.
 
-> 여기서 말하는 차단은, 호출한 runBlocking 코루틴과 그 자식 코루틴을 제외한 다른 작업이 스레드를 사용할 수 없음을 의미한다. 즉, 호출한 코루틴이 점유할동안 다른 작업이 해당 스레드에 접근이 안 된다는 거지 그 스레드가 아예 사용이 불가하다는 게 아니다.
 
runBlocking 코루틴은 runBlocking 함수 호출부의 스레드를 차단하고 사용하지만, launch 함수를 사용해 생성되는 launch 코루틴은 실행될 때 호출부의 스레드를 차단하지 않는다.
 
코루틴 실행 도중 예외가 발생하면 예외가 발생한 코루틴은 취소되고 부모 코루틴으로 예외가 전파된다. 루트 코루틴에서도 예외가 처리되지 않으면 코루틴은 취소된다. 코루틴이 취소되면 모든 자식 코루틴에게 취소가 전파된다. 
 
만약 작은 작업에서 발생한 예외로 인해 큰 작업이 취소되면 애플리케이션의 안정성에 문제가 생길 수 있다. 이런 문제 해결을 위해 코루틴은 예외 전파를 제한하는 여러 장치를 가진다. 
 
1) Job 객체를 사용해 예외 전파 제한하기
코루틴은 자신의 부모 코루틴으로만 예외를 전파하는 특성을 가지므로 부모 코루틴과의 구조화를 깬다면 예외가 전파되지 않는다.
하지만 Job 객체를 생성해 코루틴의 구조화를 깨는 것은 예외 전파를 제한하는 것뿐만 아니라 취소 전파도 제한시킨다. 
만약 작은 작업의 구조화가 깨진다면 큰 작업에 취소가 요청되더라도 작은 작업은 취소되지 않으며 이는 비동기 작업을 불안정하게 만든다.
-> 한 마디로 구조를 쪼갰기 때문에 예외 뿐만 아니라 취소도 영향이 안 가게 된다는 것
 
구조화를 깨지 않으면서 예외 전파를 제한할 수는 없을까?
 
2) SupervisorJob 객체를 사용한 예외 전파 제한
SupervisorJob 객체는 자식 코루틴으로부터 예외를 전파받지 않는 특수한 Job 객체로 하나의 자식 코루틴에서 발생한 예외가 다른 자식 코루틴에게 영향을 미치지 못하도록 만드는 데 사용된다.
-> 중간에서 끊어주는 역할
 
CoroutineScope(SupervisorJob())을 통해 하위 코루틴을 모두 SupervisorJob으로 만들 수 있다.
 
SupervisorJob 사용 시 흔히하는 실수는 예외 전파 방지를 위해 코루틴 빌더 함수의 context 인자에 SupervisorJob()을 넘기고, 코루틴빌더 함수가 호출돼 생성되는 코루틴의 하위에 자식 코루틴들을 생성하는 것이다.
 
launch 함수는 context 인자에 Job 객체가 입력될 경우 해당 Job 객체를 부모로 하는 새로운 Job 객체를 만든다.
274pg
SupervisorJob 객체를 생성할 때 SupervisorJob 객체가 Job 계층 구조의 어떤 위치에 있어야 하는지 충분히 고민하고 사용해야 한다.
 
3) supervisorScope 함수를 사용하는 것이다.
supervisorScope 함수는 SupervisorJob 객체를 가진 CoroutineScope 객체를 생성하며, 이 SupervisorJob 객체를 가진 CoroutineScope 객체를 생성하며, 이 SupervisorJob 객체는 supervisorScope 함수를 호출한 코루틴의 Job 객체를 부모로 가진다.
 
supervisorScope를 사용하면 복잡한 설정 업싱도 구조화를 깨지 않고 예외 전파를 제한할 수 있다.
supervisorScope의 SupervisorJob 객체는 코드가 모두 실행되고 자식 코루틴도 모두 실행 완료되면 자동으로 완료 처리된다.
 
280pg 왜 Job이 3개가 되는지 이해 X
 
-> CoroutineScope는 명시적으로 Job()을 컨텍스트에 포함시켜 생성합니다. Job()은 새로운 Job 객체를 생성하며, 이 객체는 스코프 내에서 시작하는 모든 코루틴의 부모 Job이 됩니다.
 
*만약 자식 코루틴이 부모 코루틴으로 예외를 전파하면 자식 코루틴에서는 예외가 처리된 것으로 봐 자식 코루틴에 설정된 CoroutineExceptionHandler는 동작하지 않는다.
-> 예외가 마지막으로 전파되는 위치에 CoroutineExceptionHandler 객체를 설정하면 된다.
 
1) Job과 CoroutineExceptionHandler 함께 설정하기
2) SupervisorJob과 CoroutineExceptionHandler 함께 사용하기
예외가 발생하더라도 SupervisorJob 객체에는 전파되지 않는다. 하지만 예외에 대한 정보는 전달받으므로 CoroutineExceptionHandler 객체가 해당 오류를 처리하게 된다.
자식 코루틴이 부모 코루틴으로 예외를 전파하지 않고 전달만 하더라도 자식 코루틴에서 예외는 처리된 것으로 본다.
 
3) CoroutineExceptionHandler는 예외 전파를 제한하지 않는다.

 

CoroutineExceptionHandler 사용 시 많이 하는 실수는 CoroutineExceptionHandler가 try catch문처럼 동작해 예외 전파를 제한한다고 생각하는 것이다. 하지만 CoroutineExceptionHandler는 예외가 마지막으로 처리되는 위치에서 예외를 처리할 뿐, 예외 전파를 제한하지 않는다.

→ 겉보기엔 CoroutineExceptionHandler가 달려있으면 그 부분에서 예외를 잡고 끝낼 것 같지만 그게 아니라 일단 전파를 다 해놓고 마지막 위치에서 잡을 수 있으면 잡는단 얘기.

 

반면 try-catch는 예외 전파를 제한한다.

→ catch에서 잡히면 그 예외는 다른 곳으로 전파되지 않고 거기서 끝난다는 얘기.

 

launch 등 코루틴 빌더 바깥을 try-catch로 감싸면 예외가 잡히지 않는다.

코루틴에 대한 예외 처리를 위해서는 코루틴 빌더 함수의 람다식 내부에서 try-catch문을 사용해야 한다는 것을 명심하고, 코루틴 빌더 함수에 try catch문을 사용하지 않도록 주의하자.

 

async 코루틴 빌더 함수는 예외가 발생했건 뭐건 그 결과를 await 호출 시 보여주기 때문에

try { deferred.await() } catch(e: Exception) {} 이런 식으로 await 호출부에서 예외를 잡아볼 수 있다.

→ 예외가 어느 시점에 노출되는지 파악하고 있을 필요가 있단 얘기.

 

async 코루틴 빌더 함수 사용 시 많이하는 실수 중 하나는 await 함수 호출부에서만 예외 처리를 하는 것이다.

→ async 코루틴 빌더 함수도 예외가 발생하면 부모 코루틴으로 예외를 전파하는데 이를 적절하게 처리해야 한다.

 

전파되지 않는 예외도 있다.

CancellationException

왜 부모 코루틴으로 예외를 전파하지 않는 것일까?

CancellationException은 코루틴의 취소에 사용되는 특별한 예외이기 때문이다.

 

Job 객체에 대해 cancel 함수를 호출하면 CancellationException의 서브 클래스인 JobCancellationException을 발생시켜 코루틴을 취소시킨다. 이처럼 CancellationException은 특정 코루틴만 취소하는 데 사용되며, 코루틴 코드상에서 다양하게 응용돼 사용된다.

 

CancellationException을 사용하는 대표적인 함수로 withTimeOut이 있다.

withTimeOut 함수는 작업(block)이 주어진 시간(timeMillis) 내에 완료되지 않으면 TimeoutCancellationException을 발생시키는데 이 역시 CancellationException의 서브 클래스이다.

 

실행 시간을 초과하더라도 코루틴이 취소되지 않고 결과가 반환돼야 한다면 withTimeOutOrNull 함수를 사용할 수 있다. 이를 사용하면 TimeOutCancellationException을 외부로 전파하는 대신 내부적으로 해당 예외 처리 후 null을 반환한다.

 

일시 중단 함수는 일시 중단 지점이 포함된 코드를 재사용이 가능한 단위로 추출하는 데 사용된다. (302~303pg)

일시 중단 함수 사용 시 많이 하는 실수 중 하나는 일시 중단 함수를 코루틴과 동일하게 생각하는 것이다. 분명한 것은 일시 중단 함수는 코루틴 내부에서 실행되는 코드의 집합일 뿐, 코루틴이 아니다.

 

코루틴에서 일시 중단이 가능한 지점은 다음 두 가지이다. 1. 코루틴 내부, 2. 일시 중단 함수

일시 중단 함수에서 launch나 async 같은 코루틴 빌더 함수를 호출하기 위해서는 일시 중단 함수 내부에서 CoroutineScope 객체에 접근할 수 있도록 해야 한다.

 

coroutineScope 일시 중단 함수를 사용하면 일시 중단 함수 내부에 새로운 CoroutineScope 객체를 생성할 수 있다. 311pg

 

Q. 314pg 이해가 잘 안 됨. supervisorScope 썼으면 된 거 아닌가? try-catch로 묶는 건 또 뭐지? 둘 중 하나만 해주면 되는 거 아닌가?

코루틴은 작업 중간에 스레드의 사용이 필요 없어지면 스레드를 양보하며, 양보된 스레드는 다른 코루틴을 실행하는 데 사용할 수 있다. 그렇다면 스레드를 양보하는 주체는 누구일까? 스레드를 양보하는 주체는 코루틴이다. 스레드에 코루틴을 할당해 실행되도록 만드는 주체는 CoroutineDispatcher 객체이지만 스레드를 양보하는 주체는 코루틴으로 CoroutineDispatcher는 코루틴이 스레드를 양보하도록 강제하지 못한다.

 

코루틴이 스레드를 양보하려면 코루틴에서 직접 스레드 양보를 위한 함수를 호출해야 한다. 만약 코루틴에서 스레드 양보를 위한 함수가 호출되지 않는다면 코루틴은 실행 완료될 때까지 스레드를 점유한다. 이런 특성 때문에 코루틴의 스레드 양보는 작업의 실행 흐름에서 중요한 역할을 한다.

 

대표적인 일시 중단 함수들로는 delay, join, await, yield가 있다.

코루틴이 delay 함수를 호출하면 코루틴은 사용하던 스레드를 양보하고 설정된 시간 동안 코루틴을 일시 중단시킨다.

Thread.sleep 함수는 delay 함수와 유사하게 일정 시간 동안 대기하는 데 사용되지만 대기 시간 동안 스레드를 블로킹시킨다.

Job의 join 함수나 Deferred의 await 함수가 호출되면 해당 함수를 호출한 코루틴은 스레드를 양보하고 join 또는 await의 대상이 된 코루틴 내부의 코드가 실행 완료될 때까지 일시 중단된다.

 

324pg 중요해보임.

 

delay나 join 같은 일시 중단 함수들은 스레드 양보를 직접 호출하지 않아도 작업을 위해 내부적으로 스레드 양보를 일으켜 개발자가 직접 세세하게 조정할 필요가 없게 한다. 하지만 몇가지 특수한 상황에서는 스레드 양보를 직접 호출해야 할 필요가 있다. 이를 위해 코루틴 라이브러리는 yield 함수를 통해 직접 스레드 양보를 실행하는 방법을 제공한다.

 

Q. 328pg 이해 안 됨. “작업 중”이 1번만 출력돼야 하는 거 아닌가?

 

코루틴의 실행 스레드는 고정이 아니다.

코루틴이 일시 중단 후 재개되면 CoroutineDispatcher 객체는 재개된 코루틴을 다시 스레드에 할당한다. 이때 CoroutineDispatcher 객체는 코루틴을 자신이 사용할 수 있는 스레드 중 하나에 할당하는데 이 스레드는 코루틴이 일시 중단 전에 실행되던 스레드와 다를 수 있다.

 

스레드를 양보하지 않으면 실행 스레드가 바뀌지 않는다.

 

코루틴의 실행 스레드가 바뀐다고 하면 그 시점은 코루틴이 재개될 때이다. 즉, 코루틴이 스레드 양보를 하지 않아 일시 중단될 일이 없다면 실행 스레드가 바뀌지 않는다.

'Android > 공부' 카테고리의 다른 글

Fragment에선 왜 viewLifecycleOwner를 써야할까  (0) 2024.09.08
retrofit의 비동기 처리  (0) 2024.04.29
Flow  (2) 2024.03.14
Repository의 효용에 대한 고찰  (0) 2023.03.16
[공부] Github Action CI  (0) 2023.03.04