-
- 코루틴은 자신이 시작된 스레드를 중단하지 않으면서 비동기적으로 실행되는 코드 블록이다.
- 스레드 관련 이벤트나 결과를 처리하기 위해 callback 함수를 작성할 필요 없이 순차적으로 코드를 작성할 수있다.
- 코루틴은 스레드를 유지하면서 코루틴이 스레드에 "할당되는 방법"을 관리한다. ->
실행중인 코루틴이 일시 정지(suspend) 되면,
코틀린 런타임이 해당 코루틴을 저장하고 이 코루틴이 사용하던 스레드를 다른 코루틴에 할당한다.
그리고 저장된 코루틴이 재개(resumed)되면, 스레드 풀의 미사용 스레드로 복원하여 다시 실행한다.
(Kotlin runtime은 JVM 상에서 Kotlin 코드를 실행하는 데 필요한 라이브러리 및 실행 환경을 제공합니다.
Kotlin runtime은 Kotlin 언어 자체의 일부이며, Kotlin 코드를 컴파일 할 때 함께 빌드됩니다.)- 모든 코루틴은 그룹으로 관리될 수있는 범위(scope)에서 실행된다
- GlobalScope : 앱의 전체 생명주기와 결합된 최상위 수준의 코루틴에서 사용된다.
액티비티가 종료되어도 계속 실행될 가능성이 있어서 웬만하면 사용하지 않는것이 좋음!
코루틴이 개별적으로 실행되고 이의 참조를 직접 관리해야한다.- 코루틴 스코프 내에서 GlobalScope를 사용하면 부모 코루틴의 컨텍스트를 덮어쓰게 되고, 아래와 같은 문제가 발생한다.
- 부모 코루틴이 취소되어도 GlobalScope는 취소되지 않는다.
- 부모 코루틴의 스코프를 상속받지 않으므로 항상 default dispatcher로 실행되고, 부모 코루틴의 컨텍스트를 따르지 않는다.
- 이는 잠재적인 메모리 누수와 불필요한 계산을 야기시킨다.
- 코루틴을 단위테스트하는 툴이 제대로 동작하지 않고, 테스트하기도 어렵다.
- 코루틴 스코프 내에서 GlobalScope를 사용하면 부모 코루틴의 컨텍스트를 덮어쓰게 되고, 아래와 같은 문제가 발생한다.
- ViewModelScope : Jetpack의 ViewModel 컴포넌트 사용시 ViewModel 인스턴스에서 사용하기 위해 제공되는 범위
ViewModel 인스턴스 내부에서 해당 스코프 내에서 실행되는 코루틴은 ViewModel 인스턴스가 소멸될때 자동으로 취소된다. - 커스텀Scope : CoroutineScope(디스패처) 로 생성되는 커스텀 Scope
- 새로운 코루틴을 만들지만, 새로운 코루틴이 완료될 때까지 이전 코루틴을 일시중단 하므로 동시에 프로세스를 실행하지 않는다.
- GlobalScope : 앱의 전체 생명주기와 결합된 최상위 수준의 코루틴에서 사용된다.
- 정지함수
- 코루틴의 코드를 포함하는 특별한 타입의 함수, suspend 키워드를 사용해 선언한다.
- 메인 스레드를 중단하지 않고 시간이 오래 걸리는 작업을 수행할 수 있다.
- suspend fun mytask(){}
- 디스패처
- Dispatchers.Main : 메인 스레드에서 코루틴을 실행한다. UI를 변경하지 않는 코루틴에 적합하다.
- Dispatchers.IO : 네트워크,디스크,데이터베이스 작업에 적합하다.
- Dispatchers.Default : CPU를 많이 사용하는 작업에 적합하다
- 코루틴 빌더 : 코루틴 컴포넌트를 통합하고 코루틴을 실행한다.
- launch : 현재 스레드를 중단하지 않고 코루틴을 시작시키며 결과를 호출 측에 반환하지 않는다.
일반 함수에서 정지함수를 호출하고 결과를 처리할 필요가 없을 때 사용한다. - async : 현재 스레드를 중단시키지 않고 코루틴을 시작시키며 호출 측에서 await()을 사용해서 결과를 기다릴 수 있다.
병행으로 실행될 필요가 있는 다수의 코루틴이 있을 때 async를 사용한다.
정지 함수 내부에서만 사용할 수 있다. - withContext : 부모 코루틴과는 다른 context에서 코루틴이 실행되도록 한다.
withContext(Dispatchers.IO) 처럼 자식 코루틴의 실행 스레드를 변경 시킨다. - coroutineScope : 병행으로 실행될 다수의 코루틴을 정지 함수가 시작시키고, 모든 코루틴이 완료될 때만 처리를 할때 사용된다.
호출 함수는 모든 자식 코루틴이 완료되어야 실행이 끝나고 복귀한다. 하나라도 실행에 실패하면 모든 다른 코루틴이 취소된다. - supervisorScope : coroutineScope와 비슷하나 자식 코루틴이 실행에 실패해도 다른 코루틴에 영향을 끼치지 않는다.
- runBlocking : 코루틴을 시작시키고 완료될때까지 현재 스레드를 중단시킨다. (사용하지 않는것이 좋다!)
- launch : 현재 스레드를 중단하지 않고 코루틴을 시작시키며 결과를 호출 측에 반환하지 않는다.
- Job : launch나 aync등 코루틴 빌더의 반환값 인스턴스
- 이 인스턴스로 코루틴의 생명주기를 관리할 수있다.
- Job 인스턴스의 isActive,isCompleted,isCancelled 속성으로 알수있음.
- cancelChildren()으로 모든 자식 코루틴을 취소시킬수 있음
- 특정 코루틴에서 빌더를 여러번 호출하게 되면 JobA가 부모, JobB가 자식 인스턴스가 되어 부모-자식 관계를 형성한다. -> JobA를 취소시키면 JobB도 취소된다. 특정 자식 Job의 취소는 부모를 취소 시키지 않는다.
- launch 빌더를 통해서 생성된 자식 Job에서 예외가 발생하면 부모 Job이 취소된다.
- async 빌더를 통해서 생성된 자식 Job에서 예외가 발생되어도 취소되지 않는다 -> 부모 Job에 반환되는 결과에 자식 Job의 예외가 포함됨!
- join()는 모든 자식 Job이 완료될 때 까지 특정 Job과 연관된 코루틴을 정지시킨다.
특정 Job을 취소 시킬때는 cancelAndJoin()을 호출한다.
- 이 인스턴스로 코루틴의 생명주기를 관리할 수있다.
코루틴의 정지와 실행
//커스텀 스코프와 launch로 코루틴 빌더 사용 private val myCoroutineScope = CoroutineScope(Dispatchers.Main) //정지함수가 아니므로 async 빌더 대신에 launch 빌더를 사용해서 코루틴을 사용한다. fun startTask(view:View){ myCoroutineScope.launch(Dispatchers.Main){ slowTask() } } //5초간 지연시킨후 로그를 출력하는 함수 suspend fun slowTask(){ Log.i(TAG, "before") delay(5000) //그러나 지연되는 5초동안 메인 스레드는 중단되지 않는다. -> UI는 계속 응답 가능 Log.i(TAG,"after") }
위에서 제일 먼저 startTask 함수가 실행되고 정지 함수인 slowTask가 코루틴으로 시작된다.
그리고 이 함수는 5초를 인자로 코틀린 delay 함수를 호출한다.
delay 함수 역시 정지 함수로 구현되었으므로, 코틀린 런타임에 의해 코루틴으로 시작된다.
이 시점에서 코드의 실행은 정지점 ( delay 코루틴이 실행될 동안 slowTask 코루틴이 정지되는 부분 )에 도달한다.
이때 slowTask가 실행되던 스레드는 사용이 해제되고 제어는 메인 스레드에 넘어간다.
따라서 UI는 계속 응답이 가능하다.
->
startTask() 함수가 호출되면 slowTask() 함수가 실행되며, slowTask() 함수에서 delay() 함수가 호출되면 5초 동안 코루틴이 일시 중단됩니다. 이 때, 메인 스레드는 중단되지 않으므로 UI는 계속 응답 가능합니다. 5초가 지난 후에는 코루틴이 다시 실행되어 로그를 출력하게 됩니다.
만약 위 코드에서 값을 반환받고 싶다면 Deferred 객체로 반환해야 한다.
Deferred 객체는 향후 언젠가 객체가 값을 제공하는 객체이다.
Deferred 객체에 await() 함수를 호출하면 해당 코루틴에서 값이 반환될 때 코틀린 런타임이 전달해 준다.
다음과 같이 수정한다.
fun startTask(view:View){ coroutineScope.launch(Dispathers.Main){ textView.text = slowTask().await() } } /* startTask는 정지 함수가 아니라 launch 빌더를 사용했지만, 코루틴에서 결과를 반환하려면 async 빌더를 사용해야 하기때문에 Deferred 객체를 반환하는 코루틴을 시작시켰다. */ suspend fun slowTask():Deferred<String>{ //slowTask() 함수는 5초 동안 대기하는 작업이며, 이 작업은 CPU-intensive 작업에 속합니다. //따라서 Dispatchers.Default를 사용하여 백그라운드 스레드에서 slowTask() 함수를 실행하는 것이 적절합니다. coroutineScope.async(Dispathers.Default){ Log.i(TAG, "before") delay(5000) Log.i(TAG,"after") return@async "FINISH" /* 레이블(labeled) return은 일반적인 return 문과 달리, 함수 내에서 중첩된 블록에서 return할 때 사용됩니다. 이를 통해, 해당 블록에서 return을 할 때 반환할 대상을 지정할 수 있습니다. return@withContext나 return@async은 해당 함수가 일시 중단된 상태에서 반환할 값이 어디로 전달될지를 명시하는 역할을 합니다. */ } }
coroutineScope.async() 빌더를 사용하여 slowTask() 함수를 선언하면 async 빌더는 백그라운드 스레드에서 실행되는 비동기 작업을 반환합니다. 이 작업은 Deferred 객체로 반환되고, await() 함수를 사용하여 작업의 결과를 기다릴 수 있습니다. async() 빌더는 Dispathers.Default를 사용하여 백그라운드 스레드에서 실행되므로, 이 함수에서 수행하는 작업은 메인 스레드를 차단하지 않습니다.
delay() 함수를 사용하여 5초 동안 코드 실행을 일시 중단하고 다시 시작합니다. 이 때, async() 빌더는 반환된 Deferred 객체를 반환합니다. return@async "FINISH" 구문은 백그라운드 스레드에서 "FINISH"를 반환하고 이를 await() 함수를 통해 startTask() 함수로 전달합니다.
따라서 startTask() 함수가 호출되면 slowTask() 함수가 실행되며, await() 함수를 통해 백그라운드 스레드에서 반환된 결과를 기다린 후에 textView.text에 할당됩니다. 이를 통해 코루틴을 사용하여 메인 스레드를 차단하지 않고 비동기적으로 작업을 처리할 수 있습니다.
코루틴 내부의 여러 자식코루틴이 있고, 각 자식 코루틴 별로 작업이 다르다면 withContext를 통해서 컨텍스트를 변경시켜줘야 한다.
아래의 코드에서
fun startTask(view:View){ coroutineScope.launch(Dispathers.Main){ tasks() } } suspend fun tasks(){ task1() task2() task3() } suspend fun task1(){ Log.i(TAG, "one") } suspend fun task2(){ Log.i(TAG, "two") someNetWorkThing() //네트워크 작업 } suspend fun task3(){ Log.i(TAG, "three") }
task2 가 네트워크 작업을 하므로 Main 디스패처에서 task2 를 호출하는것은 옳지 않다.
IO 디스패처로 컨텍스트를 변경해주어야 한다.
suspend fun tasks(){ task1() withContext(Dispatchers.IO){ task2() } task3() }
위와 같이 변경해준다.
또 async 빌더와 Deferred 객체의 await() 호출 대신 사용할 수도 있다.
앞의 코드를 다음과 같이 변경해준다.
suspend fun slowTask():Deferred<String>{ withContext(Dispatchers.Main){ Log.i(TAG, "before") delay(5000) Log.i(TAG,"after") return@withContext "FINISH" } /* coroutineScope.async(Dispathers.Default){ } */ }
채널은 데이터 스트림을 비롯해서 코루틴 간의 통신을 구현하는 간단한 방법을 제공한다.
Channel 인스턴스를 생성후 send()함수를 호출하여 데이터를 전송하기
이때 전송된 데이터는 같은 Channel 인스턴스의 receiver()함수로 다른 코루틴에서 수신할 수 있다.val ch = Channel<Int>() suspend fun demo() { couroutineScope.launch(Dispatchers.Main){task1()} couroutineScope.launch(Dispatchers.Main){task2()} } suspend fun task1(){ for (i in 1..6){ ch.send(i) } } suspend fun task2(){ repeat(6){ // 필수! Log.d(TAG , ${ch.recieve()}) } }
응답은 1 2 3 4 5 6 으로 온다.
repeat이 없다면 DeadLock 상태가 되는데,
task2()에서 ch.receive() 호출 시, ch 채널에서 값이 전달될 때까지 무한정 기다리게 됩니다. 그리고 task2() 함수는 블록되어 다음 코드가 실행되지 않으므로, task1()에서 ch.send()를 호출해도 이를 수신할 수 있는 대상이 없어서 무한정 대기하게 됩니다. 이러한 상황을 데드락(deadlock)이라고 합니다.
따라서 repeat 구문이 없으면 코드는 무한정 대기하게 되며, 예상 출력 결과는 존재하지 않습니다. 이를 방지하기 위해서는 task2()에서 ch.receive() 호출 시, 값을 기다리는 대신에 코루틴이 다른 작업을 수행할 수 있도록 delay나 다른 suspend 함수를 사용하여 일시 중단하도록 만들어주어야 합니다.
val ch = Channel<Int>() suspend fun demo() { couroutineScope.launch(Dispatchers.Main){task1()} couroutineScope.launch(Dispatchers.Main){task2()} } suspend fun task1(){ Log.d(TAG , "start") for (i in 1..6){ Log.d(TAG , "before") ch.send(i) Log.d(TAG , "after") } Log.d(TAG , "end") } suspend fun task2(){ repeat(6){ Log.d(TAG , ${ch.recieve()}) } }
다음의 결과 고민해보기
start before after before after before after before after before after before after 1 2 3 4 5 6 end
'Android' 카테고리의 다른 글
coroutine - 하나의 scope 안에서 collect를 하지 않는 이유 (0) 2023.03.30 Service (0) 2023.03.25 context (0) 2023.03.24 Intent + broadcast (0) 2023.03.24 4대 컴포넌트와 매니페스트 (0) 2023.03.20