co(함께, 동시에)라는 의미를 갖고있고, 동시성 프로그래밍 개념을 코틀린에 도입한 것이다.
코루틴이 시작된 스레드를 중단하지 않으면서 비동기적으로 실행되는 코드이다. 기존의 복잡한 AsyncTask 또는 다수 스레드 관리를 직접 해주지 않아도 되며, 기존 다중 스레드 보다 훨씬 더 효율적으로 동작한다.
코루틴은 스레드 위에서 실행되는데 여러가지 코루틴이 존재한다고 할때 코루틴 1,2,3이 있다고 치면, 코루틴1을 실행하던 중 2가 실행돼도 실행중인 스레드를 정지하면서 컨텍스트 스위칭 개념으로 다른 스레드로 전환하는 것이 아니라 기존 스레드를 유지하며 기존 스레드에서 코루틴2를 실행하게 된다.
이후 코루틴1을 다시 실행할 때 저장해둔 코루틴1 상태를 불러와 다시 스레드에서 코루틴1을 실행하게 된다. 한마디로 스레드의 멈춤없이 루틴을 돌릴 수 있게 되며 이는 성능 향상을 기대할 수 있고 여러 스레드를 사용하는 것 보다 훨씬 적은 자원 소모를 하게 된다.
왜냐하면 스레드 관련 이벤트 또는 결과 처리를 위한 콜백 작성이 필요없고 순차적으로 코드를 작성하면 되기 때문이다. (사실 코루틴도 내부적으로 여전히 다중 스레드를 사용한다.)
모든 코루틴은 스코프 안에서 실행되어야 하는데 이를 통해서 액티비티 또는 프래그먼트의 생명주기에 따라 소멸 될 때 관련 코루틴을 한번에 취소할 수 있는데 이는 곧 메모리 누수를 방지한다. 스코프는 거스텀 또는 이미 내장된 범위를 사용할 수 있다.
CoroutineScope는 CoroutineContext 타입 필드를 launch 등의 확장 함수 내부에서 사용하기 위한 매개체 역할만 담당한다.
CoroutineContext는 실제로 코루틴이 실행중이 여러 작업(Job)과 디스패처를 저장하는 일종의 맵이라고 보면된다. 이를 통해 코틀린 런타임은 다음에 실행할 작업을 고르고 어떤 스레드에 배정할지 결정한다.
모든 코루틴은 항상 자신이 속한 스코프를 참조해야한다. 이후에 canclefh 모두 취소 가능하다.
코루틴 스코프의 경우에는 GlobalScope와 CoroutineScope가 존재한다.
코루틴 스코프의 경우 글로벌 스코프와 달리 디스패쳐를 지정할 수 있는데 이는 코루틴이 실행될 스레드를 지정하는 것이다.
코루틴 디스패쳐의 경우에는 Default, IO, Main, Unconfined 등이 있다.
Dispatchers.Default: 안드로이드 기본 스레드풀 사용. CPU를 많이 쓰는 작업에 최적화 (데이터 정렬, 복잡한 연산 등)
Dispatchers.IO: 이미지 다운로드, 파일 입출력 등 입출력에 최적화 되어있는 디스패쳐 (네트워크, 디스크, DB 작업에 적합)
Dispatchers.Main: 안드로이드 기본 스레드에서 코루틴 실행. UI와 상호작용에 최적화
Dispatchers.Unconfined: 호출한 컨텍스트를 기본으로 사용하는데 중단 후 다시 실행될 때 컨텍스트가 바뀌면 바뀐 컨텍스트를 따라가는 특이한 디스패쳐
디스패처는 코루틴을 적당한 스레드에 할당하며, 코루틴 실행 도중 일시 정지 또는 실행 재개를 담당한다. (다음에 어떤 코루틴을 실행 시킬지 결정) 커스텀 스레드풀을 위한 디스패처도 생성할 수도 있다.
코루틴은 launch와 async로 시작이 가능하다. launch는 상태를 관리 할 수 있고, async는 상태를 관리 + 결과까지 반환 받을 수 있다. 코루틴을 생성하고 상태 관리 메서드를 호출해서 중단, 지연 할 수 있다.
코루틴의 동작을 멈추는 상태관리 메서드로 하나의 스코프 안에 여러 코루틴이 존재하는 경우 하위 코루틴 또한 모두 멈춥니다. 아래 코드에서 job을 캔슬하게 되면 안에 있던 job1도 중단됩니다.
fun main() {
val job = CoroutineScope(Dispatchers.Default).launch {
val job1 = launch {
for (i in 0..10) {
delay(500)
}
}
}
job.cancel()
}
delay()는 yield()와 마찬가지로 다른 코루틴에 실행을 양보하게 된다. (정해준 시간이 끝날때 까지 무한 양보한다. 즉 양보했던 코루틴이 다시 양보했더라도 delay 시간이 끝나지 않았다면 다시 양보했던 코루틴에게 제어권이 넘겨진다.)
코루틴 내부에 여러 launch 블록이 있는 경우 모두 새로운 코루틴으로 분기되어 동시 실행 되기 때문에 순서를 정할 수 없다. 순서를 정해야 한다면 join()을 사용해서 순차적으로 실행되도록 코드를 짤 수 있다.
suspend fun main() {
CoroutineScope(Dispatchers.Default).launch {
launch {
for (i in 0..10) {
delay(500)
println("1: $i")
}
}.join()
launch {
for (i in 0..10) {
delay(500)
println("2: $i")
}
}
}.join()
}
async로 코루틴 스코프의 결과를 받아서 쓸 수 있다. 특히 연산시간이 오래 걸리는 2개의 네트워크 작업의 경우를 예를 들면 2개의 작업이 모두 완료되고 나서 이를 처리하려면 await()을 사용할 수 있다. 이때는 async 작업이 모두 완료되고 나서야 await() 호출 코드가 실행된다.
suspend fun main() {
CoroutineScope(Dispatchers.Default).launch {
val deffered1 = async {
delay(500)
350
}
val deffered2 = async {
delay(500)
200
}
println("${deffered1.await()} + ${deffered2.await()}")
}.join()
}
코루틴 안에서 사용되면 suspend 함수가 호출된 경우 이전까지의 코드의 실행이 멈추며 suspend 함수가 처리가 완료된 후 멈춰 있던 원래 스코프의 다음 코드가 실행된다.
suspend fun subRoutine() {
for (i in 0..10) {
println("$i")
}
}
CoroutineScope(Dispatchers.Main).launch {
subRoutine()
}
suspend 함수를 코루틴 내에서 사용할 때 호출 이전까지 코드가 실행되고 호출이 된 순간은 해당 suspend 함수가 처리가 모두 완료되어야 후처리 코드가 실행된다. 이때 suspend 키워드를 사용했기 때문에 코루틴 스코프 안에서 자동으로 백그라운드 스레드 처럼 동작하게 된다.
suspend 키워드를 붙인 함수가 실행되면서 호출한 쪽의 코드를 잠시 멈추게 되지만 스레드의 중단이 없다.
코루틴이 실행되다가 일시 정지하는 경우 코틀린 런타임은 해당 코루틴이 실행되던 스레드에 다른 코루틴을 할당하여 실행되게 한다. 그리고 다시 이전 코루틴이 재개할 때 사용 가능한 스레드를 코틀린 런타임이 할당해준다.
만약 스레드에서 해당 코드를 사용했다면 선처리 코드가 동작하는 스레드를 멈춰야지 서브 루틴 호출이 가능한데 코루틴은 해당 부모 루틴 상태를 저장, 서브루틴 실행, 부모루틴 복원하는 식으로 동작하여 스레드 영향을 주지 않게 된다.
suspend 함수를 코루틴 스코프에서 사용할 때 호출한 스코프와 다른 디스패쳐를 사용할 때가 있는데, 호출쪽 코루틴은 Main 디스패쳐로 UI를 제어, suspend 함수는 파일 io를 하는 경우 withContext를 사용하여 suspend 함수의 디스패쳐를 IO로 변경 사용할 수 있다.
기본적으로 부모의 코루틴 디스패쳐를 사용하지만 withContext로 디스패쳐를 달리 사용할 수 있게 된다.
CoroutineScope(Dispatchers.Main).launch {
val result = withContext(Dispatchers.IO) {
readFile()
}
print("$result")
}
코틀린은 코루틴 빌더에 원하는 동작을 넘겨 코루틴을 생성하여 실행하는 방식을 사용한다.
코루틴에서 제공하는 빌더들
delay() 및 yield() 같은 미리 정의된 suspend 함수를 살펴보자.
launch, async 등 빌더를 호출하면 Job 인스턴스가 반환된다. launch는 Job을, async는 Deffered를 반환한다. Deffered는 어차피 Job을 상속한다. 코루틴 생명주기를 Job으로 관리할 수 있고, 내부에서 다시 빌더를 호출하면 자식 Job으로 생성된다. 부모 Job 취소 시 자식 Job도 취소되며 그 반대는 취소되지 않는다.
그러나 launch 빌더로 생성한 자식 Job은 예외 발생시 부모 Job을 취소 시킨다. async 빌더로 생성한 자식 Job은 예외가 발생해도 부모 Job이 취소되지 않는데 이는 반환 결과에 예외도 포함시켜버리기 때문이다.
사용할 수 있는 속성: isActice, isCompleted, isCancelled
사용할 수 있는 함수: cancel(), cancelChildren(), join(), cancelAndJoin()
join()은 모든 자식 job이 완료될 때까지 특정 Job과 연관된 코루틴을 정지한다.
cancelAndJoin()은 특정 Job을 취소한다.
스레드가 3개인 Dispatcher와 스레드가 하나인 Dispatcher를 생성했다.
val dispatcher1 = newFixedThreadPoolContext(3, "ThreadPool")
val dispatcher2 = newSingleThreadContext( "SingleThread")
스레드 풀을 직접 만들 수 있지만, 해당 스레드 풀 제어는 모두 Dispatcher에게 맡긴다. 우리가 Dispatcher에 코루틴을 보내기만 하면, Dispatcher는 스레드에 코루틴을 분산시킨다.
CoroutineScope(dispatcher1).launch {
updateButton()
}
https://whyprogrammer.tistory.com/596
https://kotlinworld.com/151
https://kotlinworld.com/150
https://developer.android.com/kotlin/coroutines?hl=ko&gclid=Cj0KCQjw4omaBhDqARIsADXULuXNkhWp7fEKgMl03IwiPkFg1z8jj44zD-HrMEVVBttzZv6p4-0P5oIaAjZqEALw_wcB&gclsrc=aw.ds