2023. 6. 10. 14:31ㆍCoroutine
https://kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine
Your first coroutine
runBlocking과 launch, delay가 있다. 각 각의 역할은 무엇일까.
글에서 말하길 runBlocking은 Builder, launch는 Scope이몀서 job, delay는 suspend fun이다.
Builder는 무엇이고 Scope는 또 무엇일까? 첫 번째 예제만으로는 부족하다.
예제 1
위의 코드와의 차이점을 생각해 볼 수 있다. Thread를 통해 sleep 하면 출력은 Hello 가 찍히고 World가 찍힌다.
sleep으로 얼마큼 delay 할까?
시간을 체크해 보자.
2초가 걸린다
실행 순서
1. Thread를 만들어 작업을 시작한다. Hello를 찍는다. Main 스레드를 sleep 시킨다 -> 이 셋은 순차적이지만 한 흐름으로 시작한다.
2. Hello가 찍힌다.
3. 1초가 지나서 thread 함수 안에 작업인 World를 찍는다.
4. 2초가 지나서 Main의 sleep이 끝난다.
5. 마지막 전체 시간이 출력된다.
예제 2
Thread와 Coroutine을 사용하면 어떤 차이가 있을까?
비교를 위해 먼저 실행을 해보자. Hello가 찍히고 World가 나온다.
시간도 2초가 걸린다.
runBlocking은 현재 thread를 Block 한다고 한다.
어떤 스레드를 Block 할까? 위의 코드에서는 어떤 Thread를 Block 할까?
println를 찍어보자.
위의 코드에 Thread를 보면 결과는 아래와 같다
1. runBlocking 안에 current Thread는 Main이고 coroutine#1이다.
2. launch 안에 있는 current Thread도 Main이지만 coroutine#2이다.
3. runBlocking 밖에 current Thread도 main이지만 coroutine은 없다.
실행 순서는 다음과 같다.
1. println in runBlocking, println Hello, delay 2000L, println in launch 거의 동시에 결과를 뱉는다.
2. 1초 뒤에 launch 안 delay가 끝나고 World를 출력한다.
3. launch는 나오지만 runBlocking 안에 dealy로 현재 Main Thread delay상태
4. delay가 끝남. 총 2초가 지났고 runBlocking 나옴
5. runBlocking 밖에 println을 찍고 main 함수 종료
만약 launch를 여러 개 생성하면?
위의 코드 runBlocking 안에 launch를 추가해 보자.
launch 2개를 추가해 총 3개의 launch를 가지고 있는 runBlocking 생성.
결과는 아래와 같다.
실행 순서
1. 먼저, runBlocking 안 첫 번째 println 이 출력됨.
2. hello를 출력, launch 3개 모두 함수 안에 println이 출력, delay가 거의 동시에 순차적으로 동작함. 이때 hello가 먼저 출력될 수도 있고 launch 안에 println이 먼저 출력될 수 있음
3. 각 launch 안에 delay 1000L 이 종료되고 그 후 World1,2,3이 출력됨. 이 때도 같은 delay여도 먼저 종료되는 launch에 따라 순서가 다르게 출력될 수 있음. 중요한 것은 world 1,2,3이 같은 흐름에 출력된다.
4. 시간을 출력하는데 약 2초가 소요됨을 알 수 있음.
5. 마지막 출력하고 main함수 종료
위를 통해 알 수 있는 것
1. runBlocking에 CoroutineContext를 지정하지 않으면 Main Thread를 Block 한다.
여기서 Block의 의미는 runBlocking 안의 작업이 다 끝날 때까지 다른 작업은 멈추고 thread를 나갈 수 없다.
마지막 launch의 delay를 6000L로 잡으면 끝날 때까지 나가지 않음을 알 수 있다.
2. runBlocking을 생성하면 coroutine 1개가 존재한다.
3.. runBlocking 안에 launch는 코루틴을 생성한다. 여기서 launch를 하면 새로운 코루틴#2 가 생성됨을 알 수 있다.
4. runBlocking 안 코드들은 순차적으로 바로 실행된다. launch를 여러 개 실행하면 여러 개의 launch가 = 여러 개의 coroutine이 거의 동시에 실행된다.
예제 3 job
job은 launch로 생성한 코루틴의 Job객체를 반환합니다.
scope의 뒤에 붙어서 launch를 실행시킨다.
위의 코드는 scope 없이 실행시킨다. 어떻게 할 수 있는 걸까?
답은 runBlocking 안에 이미 scope가 정의되어 있어서 그 안에 launch를 실행시키므로 자동적으로 launch의 scope가 정해진다.
다시 코드를 보자.
val job 변수를 만들어 launch의 반환을 담는다.
launch의 반환값은 Job객체이다. Job 객체는 무엇일까?
보통 Job이라는 건 OS입장에서 Process의 개념과 일맥상통하는데 여기서의 Job은 어떤 의미를 가질까?
Kotlin 공식 홈페이지에서는 이렇게 말한다.
백그라운드 잡이고 개념적으로 Job은 취소할 수 있는 어떠한 것인데 라이프 사이클을 가지고 있고 완료가 되면 종료한다
이해가 잘 되지 않는다. 일단 넘어가자.
Job은 상태를 갖는다.
어디서 비슷한 모양을 보았었다. Process도 이러한 상태도를 가지고 있는데 Job도 유사하게 상태도를 가지고 있다.
비슷한 개념일까? 또 일단 넘어가 보자.
Job은 Join이라는 함수가 있다. .join()함수를 쓰면 그 위치에서 Job이 완료될 때까지 기다린다.
코드를 한 번 보자.
실행 순서
1. main 함수를 실행하면 job의 launch를 실행하고 job.isActive를 출력한다 현재 true. 그리고 Hello도 출력한다.
2. 현재 job의 코루틴은 실행 중이며 delay 3000L을 처리 중이다.
3. 밖에 hello를 찍고 job.join()으로 job이 끝나길 대기 중이다.
4. 3초가 지나 job의 delay가 끝나 World를 출력한다.
5. 그리고 launch가 끝나 join()도 끝난다.
6. 마지막으로 job.isActive를 출력하고 나온다. 이 때는 job이 끝난 상태이므로 false가 출력된다.
정리하면 Job 객체는 현재 생성한 coroutine의 상태와 정보를 가지고 있는 객체가 된다. 하나의 상태를 가지고 다른 코루틴이 필요하면 대기하고 시작하고 이런 과정을 겪을 수 있다. 프로세스의 처리도 race condition, dead lock 등 자원의 문제를 다루는데 이와 비슷하다 생각이 든다.
예제 4 cancel
launch로 생성된 coroutine은 job을 반환하고 job은 cancel 가능하다. 이름처럼 도중에 취소할 수 있다.
실행 순서
1. coroutine2가 launch 되고 coroutine1은 delay 된다.
2. 1300ms가 지나기 전에 coroutine2인 job 변수 안에 println("job: I'm sleeping ~ 이 3번이 나온다.
3. 2번이 나오고 repeat 3번째에 coroutine1의 delay가 끝나 main: I'm tired of ~ 가 출력되고 바로 job이 cancel 된다.
4. job이 cancel 되어 coroutine2는 동작을 끝낸다.
5. println("job is canceled")가 출력된다.
6. job.join()인데 job이 이미 cancel 되어 끝났으므로 완료된다.
7. println("main: Now I can quit")이 마지막으로 출력되고 main함수를 끝낸다.
알 수 있는 것
coroutine을 cancel과 join을 결합하여 사용하면 좀 더 다양한 제어가 가능해진다.
중요한 것은 올바르게 cancel 하려면 안에 suspend 함수의 사용이 있어야 가능하다.
delay는 suspend function이기 때문에 launch의 cancel이 가능한 것이다.
만약 cancel과 동시에 join을 하고 싶으면 아래와 같이 사용한다.
job.cancelAndJoin()
cancelAndJoin은 아래와 같이 동작한다.
public suspend fun Job.cancelAndJoin() {
cancel()
return join()
}
내부 동작을 확인하면 그저 cancel 하고 return join 함을 알 수 있다.
또 isActive의 사용법도 나온다.
예제 5
launch 안에 while문으로 출력을 반복한다.
delay가 1300ms이므로 3개를 찍고 나온다. 그런데 cancelAndJoin 후에도 출력이 된다.
분명 cancel 되었는데 왜 launch 안에 반복문이 계속 진행될까?
답은 간단하다. cancel 하려는 코루틴 내부에서 cooperative 하지 않아서이다.
cooperative 하지 않다? 이건 또 무슨 말이라고 해야 할까?
예제 4에서 말했든 coroutine의 cancel은 생성된 코루틴 안에 cancel 가능하게 cooperative 한 방식이 있어야 한다.
말 그대로 cancel 하려면 corotine 내부에서도 "협조적"으로 나와야 한다라고 보인다.
delay는 suspend function이라 협조적이므로 취소할 수 있다.
모든 suspend func은 왜 협조적이라고 하는 걸까?
suspend function은 coroutine의 cancellation을 check 한다고 한다. 그래서 coroutine을 cancel 하면 exception을 발생시켜 동작을 stop 한다고 한다. 정리하면 coroutine 내부에 cancel 가능하게 cancallable 한 무언가를 만들거나 과정이 있어야 한다는 말이고, cooperative 하게 만들어줘야 한다. 그래서 모든 suspend function은 cancallable 하니까 괜찮고 아니면 isActive를 써라.
예제 4.1
한번 cancelAndJoin 후에 어떻게 되는지 보자 try catch로 exception을 확인해 보자.
결과는 아래와 같다.
exception이 발생하면서 동시에 멈추지 않는다. 말을 안 듣는다. 왜 안 들을까?
협조적으로 나오질 않아서다. 취소시키려면 아래와 같이 isActive를 사용하자.
예재 4.2
위의 코드를 보면 while문의 조건으로 isActive로 coroutineContext의 정보를 가지고 현재 coroutine의 활성화 여부를 확인할 수 있다. 이제 결과가 아래와 같이 정상적으로 작동한다.
'Coroutine' 카테고리의 다른 글
coroutine dispatchers, Flow Hot & Cold (0) | 2023.06.13 |
---|---|
coroutine suspend function, async, await (0) | 2023.06.12 |
Coroutine 기본 개념잡기 (0) | 2022.12.27 |