Java/Kotlin
[Coroutine] 코루틴 학습 - 9 (Coroutine scope function)
Icarus8050
2022. 5. 7. 10:05
반응형
GlobalScope
- 두 개 이상의 일시중단 함수를 병렬적으로 수행하기 위한 가장 손쉬운 방법은 GlobalScope.async()를 호출하는 것이다.
- 하지만 GlobalScope는 지양해야하는데, 우선 GlobalScope의 정의를 먼저 살펴보면..
public object GlobalScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
- GlobalScope의 정의를 살펴보면 coroutineContext가 EmptyCoroutineContext이다.
- 코루틴 스코프 내에서 GlobalScope를 사용하면 부모 코루틴의 컨텍스트를 덮어쓰게 되고, 아래와 같은 문제가 발생한다.
- 부모 코루틴이 취소되어도 GlobalScope는 취소되지 않는다.
- 부모 코루틴의 스코프를 상속받지 않으므로 항상 default dispatcher로 실행되고, 부모 코루틴의 컨텍스트를 따르지 않는다.
- 이는 잠재적인 메모리 누수와 불필요한 계산을 야기시킨다.
- 코루틴을 단위테스트하는 툴이 제대로 동작하지 않고, 테스트하기도 어렵다.
coroutineScope
- courinteScope는 스코프를 시작하는 일시중단 함수를 제공한다.
- 전달된 argument function에 의해 생성된 값을 반환한다.
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
- 새로운 코루틴을 만들지만, 새로운 코루틴이 완료될 때까지 이전 코루틴을 일시중단 하므로 동시에 프로세스를 실행하지 않는다.
fun main() = runBlocking {
val a = coroutineScope {
delay(2000)
10
}
println("a is calculated")
val b = coroutineScope {
delay(2000)
20
}
println(a)
println(b)
}
// (2 sec)
// a is calculated
// (2 sec)
// 10
// 20
- coroutineContext에서 바깥 스코프로부터 스코프를 상속받고 싶다면 context의 Job을 오버라이드하면 된다. 이 방법은 아래와 같은 효과를 가지게 한다.
- 부모로부터 컨텍스트를 상속받는다.
- 모든 자식 코루틴이 완료될 때까지 기다린다.
- 보무 코루틴이 취소되었을 때, 모든 자식 코루틴 또한 취소된다.
suspend fun longTask() = coroutineScope {
launch {
delay(1000)
val name = coroutineContext[CoroutineName]?.name
println("[$name] Finished task 1")
}
launch {
delay(2000)
val name = coroutineContext[CoroutineName]?.name
println("[$name] Finished task 2")
}
}
fun main() = runBlocking(CoroutineName("Parent")) {
println("Before")
longTask()
println("After")
}
// Before
// [Parent] Finished task 1
// [Parent] Finished task 2
// After
- 위 코드를 살펴보면 "After"가 마지막에 출력되고 있는 것을 확인할 수 있는데, coroutineScope는 자식 코루틴이 끝날 때까지 완료되지 않기 때문이다.
- CoroutineName이 자식 코루틴으로 상속되어 코루틴 이름이 출력되는 것을 확인할 수 있다.
suspend fun longTask2() = coroutineScope {
launch {
delay(1000)
val name = coroutineContext[CoroutineName]?.name
println("[$name] Finished task 1")
}
launch {
delay(2000)
val name = coroutineContext[CoroutineName]?.name
println("[$name] Finished task 2")
}
}
fun main() = runBlocking {
val job = launch(CoroutineName("Parent")) {
longTask2()
}
delay(1500)
job.cancel()
}
// [Parent] Finished task 1
- 위 코드에서는 부모 코루틴이 종료되었을 때, 자식 코루틴도 함께 종료되는 것을 확인할 수 있다.
이를 바탕으로 비동기로 병렬 처리를 하는 코드를 아래 예제와 같이 작성해볼 수 있다.
suspend fun getUserProfile(): Example_42.User = coroutineScope {
val userName = async {
delay(2000)
"Hello"
}
val profileImagePath = async {
delay(3000)
"/hello/profile-image"
}
Example_42.User(
name = userName.await(),
profileImagePath = profileImagePath.await()
)
}
fun main() = runBlocking {
val userProfile = async(CoroutineName("Parent")) {
getUserProfile()
}
println(userProfile.await())
}
// (3 sec)
// User(name=Hello, profileImagePath=/hello/profile-image)
Coroutine scope functions
Coroutine builder와 Coroutine scope functinos의 차이는 아래의 테이블과 같다.
Coroutine builders (runBlocking 제외) | Coroutine scope functions |
launch, async, produce |
coruotineScope, supervisorScope, withContext, withTimeout |
CoroutineScope의 확장 함수 | 일시중단 함수 |
CoroutineScope의 리시버로부터 coruotine context를 받는다. | 일시중단 함수의 continuation으로부터 coroutine context를 받는다. |
예외는 Job을 통해 부모로 전파된다. | 익셉션 발생 시, 일반 함수처럼 동작한다. |
비동기 방식으로 코루틴을 시작한다. | 제자리에서 실행되는 코루틴을 시작한다. |
여기서 추가적으로 runBlocking과 coroutine scope function을 비교하면 다음과 같다.
- runBlocking은 해당 본문이 호출하고 값을 반환한다.
- runBlocking은 함수를 블로킹하는 반면에, coroutine scope functions은 일시중단 시킨다.
- runBlocking은 코루틴의 최상단에 위치해야 하고, coroutine scope function은 중간에 있어야 한다.
withContext
- withContext는 추가적으로 스코프를 변경시킨 coroutineScope와 유사하다.
- 이 함수에 대한 인수로 제공된 컨텐스트는 상위 스코프의 컨텍스트에 추가된다.
fun CoroutineScope.log(text: String) {
val name = this.coroutineContext[CoroutineName]?.name
println("[$name] $text")
}
fun main() = runBlocking(CoroutineName("Parent")) {
log("Before")
withContext(CoroutineName("Child 1")) {
delay(1000)
log("Hello 1")
}
withContext(CoroutineName("Child 2")) {
delay(1000)
log("Hello 2")
}
log("After")
}
// [Parent] Before
// [Child 1] Hello 1
// [Child 2] Hello 2
// [Parent] After
supervisorScope
- coroutineScope처럼 바깥 스코프로부터 상속을 받은 CoroutineScope를 생성하고, 이 범위로 지정된 일시중단 블록을 호출한다.
- coroutineScope와의 차이점은 Job의 컨텍스트를 SupervisorJob으로 오버라이드 한다는 점이다. 따라서 자식 코루틴에서 발생한 예외로 인해 취소되지 않는다.
- 서로 독립된 여러 테스크를 동시에 실행할 때 주로 사용한다.
fun main() = runBlocking {
println("Before")
supervisorScope {
launch {
delay(1000)
throw Error()
}
launch {
delay(2000)
println("Done")
}
}
println("After")
}
// Before
// (1 sec)
// Exception...
// (1 sec)
// Done
// After
만약 supervisorScope 대신 withContext(SupervisorJob())을 사용하면 같은 기능으로써 대체가 가능할까?
fun main() = runBlocking {
println("Before")
withContext(SupervisorJob()) {
launch {
delay(1000)
throw Error()
}
launch {
delay(2000)
println("Done")
}
}
println("After")
}
// Before
// (1 sec)
// Exception...
withContext(SupervisorJob())은 여전히 일반적인 Job()을 사용하고, SupervisorJob()은 부모가 된다. 그 결과로, 자식 코루틴 중 하나가 예외를 발생시켰을 때, 다른 자식들도 함께 취소된다. withContext 또한 예외를 던진다.
withTimeout
- scope를 생성하고 값을 리턴한다.
- coroutineScope와 차이점은 본문의 실행을 위한 제한 시간을 추가적으로 설정할 수 있다는 점이다.
- 만약 실행 시간이 너무 길어진다면, 본문의 실행을 취소시키고 TimeoutCancellationException을 던진다.
- TimeoutCancellationException은 CancellationException의 서브타입이다.
suspend fun test(): Int = withTimeout(1500) {
delay(1000)
println("Still thinking")
delay(1000)
println("Done")
42
}
suspend fun main(): Unit = coroutineScope {
try {
test()
} catch (e: TimeoutCancellationException) {
println("Cancelled")
}
delay(1000)
}
// Still thinking
// Cancelled
- withTimeout() 함수는 테스트에서 유용하다. 어떤 기능이 수행하는데 시간이 더 걸리는지, 덜 걸리는지 테스트하는데 사용할 수 있다.
- runBlockingTest 내부에서 사용된다면, 가상 시간으로 계산이 되어 수행한다.
- 설정한 타임아웃 시간이 초과되었을 때, CancellationException의 서브타입인 TimeoutCancellationException을 던지므로 해당 코루틴만 취소되고, 그 부모 코루틴에는 영향을 주지 않는다.
class Test {
@ExperimentalCoroutinesApi
@Test
fun testTime1() = runTest {
withTimeout(10000) {
delay(9000) // virtual time
}
}
@ExperimentalCoroutinesApi
@Test
fun testTime2() = runTest {
shouldThrow<TimeoutCancellationException> {
withTimeout(1000) {
delay(1100) // virtual time
}
}
}
@Test
fun testTime3() = runBlocking {
withTimeout(1000) {
delay(900) // 실제 900ms를 기다린다..
}
}
}
suspend fun main(): Unit = coroutineScope {
launch {
launch {
delay(2000)
println("Will not be printed")
}
try {
withTimeout(1000) {
delay(1500)
}
} catch (e: TimeoutCancellationException) {
println("Thrown TimeoutCancellationException")
}
}
launch {
delay(2000)
println("Done")
}
}
// Thrown TimeoutCancellationException
// Done
// Will not be printed
참고자료
https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837
반응형