Post

(Kotlin Coroutine) GlobalScope 단점과 대안

GlobalScope가 무엇인가? GlobalScope를 쓰는게 적절한 use-case는?

  • Global scope is used to launch top-level coroutines which are operating on the whole application lifetime and are not cancelled prematurely. Another use of the global scope is operators running in Dispatchers.Unconfined, which don’t have any job associated with them.
  • There are limited circumstances under which GlobalScope can be legitimately and safely used, such as top-level background processes that must stay active for the whole duration of the application’s lifetime. Because of that, any use of GlobalScope requires an explicit opt-in with @OptIn(DelicateCoroutinesApi::class), like this:
1
2
3
4
5
6
7
8
// A global coroutine to log statistics every second, must be always active
@OptIn(DelicateCoroutinesApi::class)
val globalScopeReporter = GlobalScope.launch {
    while (true) {
        delay(1000)
        logStatistics()
    }
}

GlobalScope의 단점, 잘못 쓰는 경우와, 대안은?

  • Application code usually should use an application-defined CoroutineScope. Using async or launch on the instance of GlobalScope is highly discouraged.
  • In top-level code, when launching a concurrent operation from a non-suspending context, an appropriately confined instance of CoroutineScope shall be used instead of a GlobalScope.
  • GlobalScope는 application 전체의 시작/종료 lifecycle과 동일한 작업에 사용하는 용도이기 때문에, applicaion이 종료 될 때 까지 무한히 돌아야 하는 작업이 아닌 한 GlobalScope를 쓰는 것은 부적절하다.

단점 1. GlobalScope.cancel() 이 불가능하다.

  • CoroutineScope.cancel() 호출하면, 해당 스코프 내 코루틴들이 모두 취소된다. 보통은 object 소멸 시 cancel() 호출해준다.
  • 어떤 작업이나 object의 lifecycle에 맞는 CoroutineScope를 써서 종료/소멸 상황에 작업 중이던 코루틴들도 같이 취소되게끔 하는게 좋은데, GlobalScope.cancel()은 에러 발생하여 스코프 내 코루틴 전체 취소가 불가능하다.
    • GlobalScope는 애플리케이션 전체 생명주기와 일치하는 스코프인데 cancel 하는 것 자체가 이상하다.
  • cancel()이 불가하다는 단점은 이런 경우 치명적이다.
    • e.g., Android activity가 종료되면 더 이상 의미가 없는 비동기 데이터 조회 작업을 GlobalScope에서 수행하면, Activity가 종료되어도 데이터 조회 작업을 계속 수행한다. 더구나 주기적 반복 작업이면 leak이 된다.
    • e.g., wss 연결을 유지하기 위해 Ping 보내는 것을 GlobalScope로 처리하게 되면, wss 연결에 문제가 있어 해제하고 다시 커넥션을 생성했을 때. 기존 GlobalScope 코루틴도 함께 취소해주어야 하는데, 내부에서 커넥션이 해제되었는지 감시가 가능하다면 내부에서 break해서 작업을 종료하는게 가능하지만, 외부에서 취소해주고 싶은 경우라면? GlobalScope 사용하는 경우 외부 취소가 불가능하다.

단점 2. GlobalScope에 coroutineContext를 매번 지정해주어야 한다.

1
GlobalScope.launch(coroutineExceptionHandler + Dispatchers.IO) { ... }
  • GlobalScope는 싱글턴이고, EmptyCoroutineContext로 고정되어 있어 그 자체에 기본 coroutineContext를 지정 할 수는 없다. (매번 명시해야 해서 번거롭다)

대안?

  • Do not replace GlobalScope.launch { ... } with CoroutineScope().launch { ... } constructor function call. The latter has the same pitfalls as GlobalScope.
  • 필요한 경우 전체 취소에 대한 제어와 handler와 Dispatchers에 대한 기본값 설정이 가능한 별도 CoroutineScope를 정의하는게 좋아보인다.
1
2
3
val IOCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + commonCoroutineExceptionHandler)
IOCoroutineScope.coroutineContext.job.join()  // Wait for all children to finish
IOCoroutineScope.cancel()  // 스코프 내 코루틴 전체 취소
1
2
IOCoroutineScope.launch { ...coroutine1... }
IOCoroutineScope.launch { ...coroutine2... }

SupervisorJob()이어야 서로 다른 사용처에서 launch한 작업들의 실패가 서로 영향받지 않는다. (coroutine1에서 발생한 ex가 coroutine2까지 취소하지 않는다)
supervison 속성은 하위 코루틴으로 상속되지 않는다는 점에 주의한다.
(e.g., 필요하다면 launch 내에서 또 다시 supervisorScope로 감싸주어야 한다.)

This post is licensed under CC BY 4.0 by the author.