개발자들은 항상 앱이 block되는 것에 대해 고민해왔다!
네트워킹 요청 등 long running task를 메인 스레드에서 처리하면 앱이 느리게 느껴지거나 ANR이 발생한다.
이런 상황을 예방하기위해 다음 비동기 프로그래밍 방식을 수십년 간 고안해왔다.
long running process를 분리된 스레드에서 진행하는 방법이다. 이 방법은 다음 drawback들이 존재한다.
함수를 매개변수로 전달하고, 프로세스가 종료되면 그 함수를 invoke하는 방식이다. 이 방법 또한 다음 이슈들이 존재한다.
위 비동기 프로그래밍 방법을 기반으로 한 thin language feature인 코루틴이 고안되었다. 이 코루틴에 대해 자세히 알아보겠다!
이걸 알고싶어서 일주일간 틈틈이 알아봤다. 근데 다들 효과만 설명하고 ㅜㅜ 답답한 부분이 너무 많았다.. 그러다 찾은 2018 드로이드나이츠 강연..! 도창욱님 께서 아주 답답했던 부분을 잘 알려주셨다..! 감사합니다 🙇🏻♀️
Blocking code를 처리하기 위해 Thread를 많이 사용한다. 하지만, 위에서 언급한 것과 같이 Thread는 다음 단점들이 있다.
게다가, Thread 안에서도 Blocking이 일어날 수 있다!
Blocking?
어떤 일을 기다리며 그 흐름이 block 되는 상황
ex. 한 스레드 내에서 네트워크 요청을 보내면 그 스레드는 응답이 돌아올 때 까지 block 되어있다.
Blocking 문제를 어떻게 해결할 수 있을까?
일단 Subroutine을 알아보자
단일 시작지점과 종료지점을 갖는다. 시작과 종료 사이에 blocking이 발생하는 것이다! 이 blocking을 없애기 위해서는 어떻게 해야할까?
이렇게 중간에 나갔다가 다시 들어온다면?? blocking 시점에서 멈춤(suspend)했다가 해당 지점에서 재개(resume)하자!
이렇게 routine끼리 협력한다는 의미에서 co-routine이라는 이름을 갖게되었다
우선, 작동원리를 알아볼 예제 코드이다.
suspend fun fetchUserDetail(id: String) {
val token = auth()
val user = getUser(token, id)
updateUserData(user)
}
// 단, auth와 getUser는 suspend 키워드가 붙은 함수이다.
위 goal을 적용하면 이 함수는 다음과 같이 진행되어야 한다.
자 이제 어떻게 suspend되고 resume되는지 진짜 알아보자.
함수가 실행될 때 다음 상태들을 갖고있는다.
그럼 위 상태들을 저장하면 다시 시작할 수 있지 않을까? ContextSwitching이 일어날 때 PCB, TCB에 저장하는 것처럼 말이다!
상태를 저장하고 전달하는 것을 Continuation Passing Style이라고 한다.
Continuation란 뭘까? 코루틴에서는 PCB, TCB와 같은 역할을 하는 것이다.
public interface Continuation<in T> {
public val context: ConrotineContext
public fun resume(value: T)
public fun resumeWithException(exception: Throwable)
}
위 fetchUserDetail 함수가 컴파일 되면 어떻게 되는지 간단히 확인해보자
fun fetchUserDetail(id: String) {
// ..
switch (label) {
case 0: val token = auth()
case 1: val user = getUser(token, id)
case 2: updateUserData(user)
}
}
suspend 키워드가 없어지고 switch문이 추가되었다. label 값에 따라 실행 위치가 달라진다! 저 label 값을 Countinuation으로 관리하는 것이다. 상태를 관리하는 부분 코드까지 같이 확인해보자
fun fetchUserDetail(id: String, cont: Continuation) {
val sm = object: CoroutineImpl { ... }
switch (label) {
case 0:
sm.id = id
sm.label = 1 // 다음 실행될 부분 지정 ❗️❗️❗️
val token = auth()
case 1:
val id = sm.id
val token = sm.result as String
sm.label = 2
val user = getUser(token, id)
case 2: updateUserData(user)
}
}
이렇게 CPS transform을 통해 suspend, resume하는 것을 확인할 수 있다!
따라서, Sequencial code를 통해 non-blocking 코드를 작성할 수 있다❗️❗️
// 콜백 사용 시
inChannel.read(buf) {
process(buf, bytesRead)
// ...
outChannel.write(buf) {
// ...
outFile.close()
}
}
// 👉 sequencial 하지 않음
// 코루틴 사용 시
launch(CommonPool) { // 코루틴 빌더로 시작
while (true) {
val bytesRead = inFile.aRead(buf) // 여기서 멈추고 코드를 나가버림 => 다른 게 thread를 사용할 수 있도록 하고 나중에 이 지점에서 resume
if (bytesRead == -1) break
// ...
process(buf, bytesRead)
outFile.aWrite(buf)
// ...
}
// sequencial 하게 작성된 코드
}