쓰레드는 각 태스크에 해당하는 스택 메모리를 할당받는다. 그리고 여러 작업을 동시에 수행해야할 때 OS 는 어떤 쓰레드 작업을 먼저 수행할지, 어떤 쓰레드를 더 많이 수행해야 효율적인지에 대한 스케쥴링 (선점 스케쥴링, Preempting Scheduling) 을 해야 한다.
Coroutuine 은 경량화 Thread 라고 부른다. 마찬가지로 작업 하나하나를 효율적으로 분배해서 동시성을 보장하는 것을 목표로 하지만, 작업 하나하나에 Thread 를 할당하는 것이 아닌 'Object' 를 할당해주고, 이 Object 를 자유롭게 스위칭함으로써 Context Switching 비용을 대폭 줄인 것이다.
1]
Thread A 에서 Task 1 을 수행하다가 Task 2 의 결과가 필요할 때, 비동기적으로 Thread B 를 호출을 하게 된다. 이 때 Thread A 는 블로킹되고, Thread B 로 Context Switching 이 일어나 Task 2 를 수행한다. Task 2 가 완료되면, 다시 Thread A 로 Context Switching 이 일어나 결과값을 Task 1 에 반환한다.
2]
쓰레드는 동시에 같이 수행할 Task 3, Task 4 는 각각 Thread C 와 D 에 할당되고, 선점 스케줄링(Preempting Scheduling)을 통하여 각 태스크를 알맞게 처리 하여 동시성을 보장하도록 한다.
Coroutine 은 이와 같은 Context Switching 과정이 없다.
따라서 매우 적은 오버헤드로 매우 높은 수준의 동시성을 제공할 수 있다.
작업을 Coroutine Object에 할당하여 처리하는데, 이때 Context Switching 과정이 아닌, Programmer Switching (OS에서 관여하는게 아닌 코드를 통해 switch 시점을 정할 수 있다.)을 사용하여 동시성을 보장한다.
Suspend (Non-Blocking) : Object 1 이 Object 2 의 결과가 나오기까지 기다려야 한다면, Object 1 은 Suspend 되지만, Object 1 을 수행하던 Thread 는 그대로 유효하기 때문에 Object 2 도 Object 1 과 동일한 Thread 에서 실행될 수 있음
같은 프로세스 내의 Heap 에 대한 Locking 걱정 또한 사라짐
작업 단위는 Coroutine Object 이므로, Task 1 을 수행하다가 비동기 작업 Task 2 가 발생하더라도, Context Switching 없이 같은 Thread 에서 Task 2 를 수행할 수 있고(작업의 단위를 Object 로 축소하면서 하나의 Thread 가 다수의 코루틴을 다룰 수 있다)
그림 오른쪽을 보면 Thread A 와 C 가 동시에 수행되는 모습이다. 이와 같은 경우에는 동시성 보장을 위해서 Context Switching 이 필요하기 때문에 단일 Thread 에서 여러 Coroutine Object 를 컨트롤하는 것을 권장한다.
코루틴은 협력작업, 예외, 이벤트 루프, 반복자, 무한 목록 및 파이프와 같은 친숙한 프로그램 구성 요소를 구현하는 데 적합하지만 이렇게 말하면 어느 타이밍에 사용할지 모르니 아래 예시를 보도록 하자
비동기(콜백, 캔슬, 리소스 관리 등)로 실행하는 코드를 간단하게 만들어 준다
main thread가 blocking 되는 부분에서 도움을 줄 수 있다.
콜백지옥으로 되어있는 코드들을 순차적으로 바꿔줄 수 있다.
비동기 코드를 순차적으로 만들 수 있다.
예시 The Dream Code
val user = fetchUserData()
textView.text = user.name
--> fetchUserData에서 네트워크 콜을 발생시키기 때문에 NetworkOnMainThreadException 이 발생
-> 이 문제를 해결하기 위해 Thread를 사용
thread{
val user = fetchUserData()
textView.text = user.name
// Ui를 UI thread에서 업데이트 해야하는데 별도의 스레드에서 업데이트를 진행하니
CalledFromWrongThreadException이 발생
}
-> 결국 위와 같이 만들기 위해서는 fetchUserData라는 함수를 만들어서 Thread를 이용해서 서버를 콜하고 다시 Main Thread로 콜백을 해서 스위치를 준 다음에 UI를 업데이트하는 작업을 해야한다.
--> 니는 결국 콜백지옥을 만들게 되고 메모리 관리도 힘들게 만든다. 결국 메모리 관리를 위해서는 많은 cancel 처리를 해줘야하기도 한다.
이 부분을 코루틴으로 해결할 수 있게 한다.
코루틴은 비동기처리를 간단하게 해주고 콜백을 대체할 수 있다.
16년도에는 코루틴은 아직 실험적이었고, 18년 10 29일 날 안정화된 버전에 배포가 된다.
20년 부터 android에서 코루틴을 권장 하고 있다.
basic 1
// 이 부분이 코루틴
// launch : 코루틴 빌더
// 내부적으로 코루틴을 만들어서 반환 해준다.
// launch 를 실행하기 위해서는 GlobalScope가 필요하다
//
// GlobalScope : 코루틴 스코프
GlobalScope.launch{
delay(1000L)
println("World!")
}
// Thread를 사용해도 위의 코드와 동일하게 작용
// 본질적으로 코로틴은 경량 스레드라고 생각하면 된다.
thread {
Thread.sleep(1000L)
println("World!")
}
println("Hello, ")
basic 2
// delay : suspend fun : 일시 중단 함수
GlobalScope.launch{
delay(1000L)
println("World!")
}
// delay는 스코프 함수나 suspend fun 함수에서 실행될 수 있다.
// runBlocking : thread를 blocking을 할 수 있는 코루틴 함수
println("Hello, ")
// Thread.sleep(2000L) 코드를 코루틴 코드로 변경
runBlocking{
delay(2000L)
}
basic 3
// 위 코드를 더 간결하게 하용
// main안의 코드가 끝까지 전까지는 리턴하지 않도록 만듦
fun main() = runBlocking {
GlobalScope.launch {
delay(1000L)
println("World!")
}
// 만약 위의 코드의 delay가 3000 이라면 3초가 지나기전에 코드가 끝나버린다.
println("Hello, ")
delay(2000L)
}
basic 4
fun main() = runBlocking {
val job = GlobalScope.launch {
delay(1000L)
println("World!")
}
println("Hello, ")
// job이 완료될 때 까지 기다리게 된다.(job의 코루틴이 완료될때까지 기다렸다가 main함수를 종료시킨다)
job.join()
}
basic 5
Stuctured concurrency
fun main() = runBlocking {
val job = GlobalScope.launch {
delay(1000L)
println("World!")
}
val job2 = GlobalScope.launch {
delay(1000L)
println("World!")
}
println("Hello, ")
job.join()
job2.join()
// 위와 같은 형태로 동시 작업을 하기 위해서는
// job을 계속 join해줘서 관리를 해줘야한다
// 이런 이유는 job을 GlobalScope를 사용해서 만들어 줬기 때문인데,
// 이런식으로 하지 말고, 상위에 있는 runBlocking과 launch와의 관계를 만들어주면
// join을 따로 하지 않아도 job이 다 끝날때 까지 기다리게 된다.
// - 방법 : runBlocking에서 들어온 scope를 사용해서 launch 한다.
// 부모 코루틴이 자식 코루틴이 완료될 때 까지 기다려주기 때문에 구조인 형태를 이용해서 코 루틴을 관리한다.
val job = this.launch {
delay(1000L)
println("World!")
}
// this 를 사용하지 않아도 됨
val job2 = launch {
delay(1000L)
println("World!")
}
println("Hello, ")
}
basic 6
// suspend 함수 만들기
fun main() = runBlocking {
launch {
myWorld()
}
println("Hello, ")
}
//fun myWorld(){
// // 예러 이유 : 코루틴도 아니고 suspend 함수도 아닌데 호출했기 때문
// delay(1000L)
// println("World!")
//}
suspend fun myWorld(){
delay(1000L)
println("World!")
}
basic 7
// 코루틴의 경량화 정도를 보는 코드
fun main() = runBlocking {
repeat(100_000) {
launch {
delay(1000L)
print(".")
}
}
// 아래 코드와 비교하면 얼마나 코루틴이 가벼운지 알 수 있다.
// out-of-memory error가 발생할 수 있음.(반복수가 더 커질 경우에)
// repeat(100_000) {
// thread {
// Thread.sleep(1000L)
// print(".")
// }
// }
}
basic 8
// 프로세스가 끝나면 코루틴도 끝남
fun main() = runBlocking {
GlobalScope.launch {
repeat(100) {i ->
// 0.5초 마다 print를 하지만 프로세스가 종료되면 코루틴은 종료 된다.
print("I'm sleeping $i ...")
delay(500L)
}
}
// 1.3초 후에 프로세스가 종료
delay(1300L)
}
basic 9
// 코루틴의 일시중지, 재개를 살펴보자
// delay가 따로 없는 경우
//fun main() = runBlocking {
// launch{
// repeat(5){i ->
// println("Coroutine A, $i")
// }
// }
//
// launch{
// repeat(5){i ->
// println("Coroutine B, $i")
// }
// }
//
// println("Coroutine Outer")
//}
// A 코루틴에 delay
//fun main() = runBlocking {
// launch{
// repeat(5){i ->
// println("Coroutine A, $i")
// delay(10L)
// }
// }
//
// launch{
// repeat(5){i ->
// println("Coroutine B, $i")
// }
// }
//
// println("Coroutine Outer")
//}
// A,B 번갈아가면서 delay 줘보기
fun main() = runBlocking {
launch{
repeat(5){i ->
println("Coroutine A, $i")
delay(10L)
}
}
launch{
repeat(5){i ->
println("Coroutine B, $i")
delay(10L)
}
}
println("Coroutine Outer")
}
// 어느 스레드에서 출력되는지 알기 위해 println을 overriding 함
// -Dkotlinx.coroutines.debug 디버그 옵션을 넣어서 어떤 코루틴에서 실행되는지 알 수 있음.
fun <T>println(msg:T){
kotlin.io.println("$msg [${Thread.currentThread().name}]")
}
정리
Coroutine builder : 코루틴을 실행시키기 위해 사용
anync, await를 통해서 비동기, 동기 조절 가능
CoroutineStart.LAZY를 통해 지연 시킬 수 있고
start()함수를 실행시켜 지연된 suspend fun을 실행 시킬 수 있다.
부모 코루틴의 책임
자식 코루틴이 끝날 때 까지 기다려준다.(join을 사용하지 않아도 된다)
좋은 글 감사합니다.