https://kotlinlang.org/docs/coroutines-guide.html
Coroutines guide | Kotlin
kotlinlang.org
https://speakerdeck.com/taehwandev/kotlin-coroutines
Kotlin coroutines
Jetbrains day in seoul 2018
speakerdeck.com
비동기는 알면 유용하게 쓰인다...
코루틴은 원래 코틀린 기능이 아니었지만 1.3버전에서 정식 채용된 플러그인.

# 수도코드
generator countdown(n) {
while(n>0) {
yield(n)
n -= 1
}
}
for i in countdown(10) {
println(i)
}
yield를 실행해서 실행하던걸 멈추고 메인루틴으로 돌아가고 다시 실행하면 중단했던 곳 부터 다시 시작한다.
kotlinx.corutines.CoroutineScope.launch
사실 CorutineScope 안엔 launch 함수 하나밖에 없어서 CorutineScope는 CoroutineContext를 실행시키는 매개체에 불과하다.
import kotlinx.coroutines.*
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
fun now() = ZonedDateTime.now().toLocalTime().truncatedTo(ChronoUnit.MILLIS)
fun log(msg:String) = println("${now()} [${Thread.currentThread().name}] $msg")
fun launchInGlobalScope() {
GlobalScope.launch {
log("launchInGlobalScope")
}
}
fun main(args: Array<String>) {
log("main start")
launchInGlobalScope()
log("launchInGlobalScope() called")
Thread.sleep(5000L)
log("main end")
}
/*
21:13:24.644 [main] main start
21:13:24.682 [main] launchInGlobalScope() called
21:13:24.684 [DefaultDispatcher-worker-1] launchInGlobalScope
21:13:29.687 [main] main end
*/
GlobalScope를 사용할 시의 주의점은 main 스레드가 끝나면 생성되었던 서브루틴도 같이 종료된다. 그래서 sleep이 없었으면 log도 안만들어보고 끝났을 것. 메인의 로그가 먼저 실행되는 이유는 launch를 실행할 때 메인스레드의 제어를 main()에 돌려주기 때문이라고 함.
이를 방지하려면 끝날때까지 기다리는 runblocking같은걸 써야 한다.
fun launchInGlobalScope() {
runBlocking {
GlobalScope.launch {
log("launchInGlobalScope")
}
}
}
fun main(args: Array<String>) {
log("main start")
launchInGlobalScope()
log("launchInGlobalScope() called")
Thread.sleep(5000L)
log("main end")
}
/*
21:18:15.482 [main] main start
21:18:15.552 [DefaultDispatcher-worker-1] launchInGlobalScope
21:18:15.553 [main] launchInGlobalScope() called
21:18:20.558 [main] main end
*/
그럼 그냥 서브루틴(일반함수) 쓰는것과 뭐가 다르냐? yield를 써서 스레드를 서로에게 넘겨주며 진행할 수 있다. 그래서 일반 함수에 yield 같은거 쓰면 컴파일 에러 뜬다. launchBlocking같은거랑 같이 쓰거나 비동기 함수에 써야함.
fun yieldExample() {
runBlocking {
launch {
log("1")
yield()
log("3")
yield()
log("5")
}
log("after first launch")
launch {
log("2")
delay(1000L)
log("4")
delay(1000L)
log("6")
}
log("after second launch")
}
}
fun main(args: Array<String>) {
yieldExample()
}
/*
21:20:52.046 [main] after first launch
21:20:52.060 [main] after second launch
21:20:52.061 [main] 1
21:20:52.062 [main] 2
21:20:52.067 [main] 3
21:20:52.067 [main] 5
21:20:53.072 [main] 4
21:20:54.074 [main] 6
*/
yield로 넘겨줬지만 delay로 기다리느라 다시 제어권이 넘어가서 로그 출력이 숫자 차례대로 출력되지 않는다.
kotlinx.coroutines.CoroutineScope.async
async, await는 자세한 예시는 안나오지만 다른 언어와 비슷한 것 같다. 사실상 async와 위의 launch와 같은 일을 한다. 유일한 차이는 launc가 Job을 반환하는 반면 async는 Deffered를 반환한다는 점 뿐이다.
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
fun sumall() {
runBlocking {
val d1 = async { delay(1000L); 1 }
log("after async(d1)")
val d2 = async { delay(1000L); 2 }
log("after async(d2)")
val d3 = async { delay(1000L); 3 }
log("after async(d3)")
log("sum is ${d1.await() + d2.await() + d3.await()}")
log("after await all & add")
}
}
fun main(args: Array<String>) {
sumall()
}
/*
21:39:57.052 [main] after async(d1)
21:39:57.079 [main] after async(d2)
21:39:57.080 [main] after async(d3)
21:39:58.103 [main] sum is 6
21:39:58.103 [main] after await all & add
*/
총 3초를 기다려야 할 것 같지만 1초만 기다림. 비동기적으로 실행되었기 때문. 프로젝트가 커지거나 실행시간이 긴 I/O같은거에 async/await를 쓰면 효과적이다.
launch, async 등은 모두 CoroutineScope의 확장 함수다. CoroutineScope 안에는 CoroutineContext를 실행하기 위한 도구일 뿐.
CoroutineContext는 실제로 코루틴이 실행 중인 여러 작업(Job 타입)과 디스패치를 저장하는 일종의 맵이라 할 수 있다.
fun log(msg:String) = println("${now()} [${Thread.currentThread().name}] $msg")
fun test() {
runBlocking {
launch { // 부모 컨텍스트를 사용(이 경우 main())
log("main runBlocking")
}
launch(Dispatchers.Unconfined) { // 특정 스레드에 종속되지 않음 ? 메인 스레드 사용
log("Unconfined runBlocking")
}
launch(Dispatchers.Default) { // 기본 디스패처를 사용
log("Default")
}
launch(newSingleThreadContext("MyOwnThread")) { // 새 스레드를 사용
log("newSingleThreadContext")
}
}
}
fun main(args: Array<String>) {
test()
}
/*
21:49:56.633 [main] Unconfined runBlocking
21:49:56.646 [DefaultDispatcher-worker-1] Default
21:49:56.653 [MyOwnThread] newSingleThreadContext
21:49:56.653 [main] main runBlocking
*/
delay()나 yield()처럼 멈추는 다른 함수들
함수 | 설명 |
withContext | 다른 컨텍스트로 코루틴을 전환한다. |
withTimeout | 코루틴이 정해진 시간 안에 실행되지 않으면 예외를 발생시키게 한다. |
withTimeoutOrNull | 코루틴이 정해진 시간 안에 실행되지 않으면 null을 결가로 돌려준다. |
awaitAll | 모든 작업의 성공을 기다린다. 작업 중 어느 하나가 예외로 실패하면 awaitAll도 그 예외로 실패한다. |
joinAll | 모든 작업이 끄탈 때까지 현재 작업을 일시 중단시킨다. |
'CS > 코틀린 인 액션' 카테고리의 다른 글
11. DSL 만들기 (0) | 2023.05.16 |
---|---|
10. 애노테이션과 리플렉션 (0) | 2023.05.10 |
9. 제네릭스 (0) | 2023.05.05 |
8. 고차 함수: 파라미터와 반환 값으로 람다 사용 (0) | 2023.05.05 |
7. 연산자 오버로딩과 기타 관례 (0) | 2023.05.03 |