- Coroutine builder
- launch
- runBlocking
- Scope
- CoroutineScope
- GlobalScope
- Suspend function
- suspend
- delay()
- join()
- Structured concurrency
fun main() {
GlobalScope.launch { // launch a new coroutine in background and continue
delay(1000L) // non-blocking delay for 1 second
println("World!") // print after delay
}
println("Hello") // main thread continues while coroutine is delayed
Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}
fun main() {
GlobalScope.launch {
delay(1000L) // non-blocking delay for 1 second
println("World!") // print after delay
}
println("Hello") // main thread continues while coroutine is delayed
runBlocking {
delay(2000L) // block main thread for 2 seconds to keep JVM alive
}
}
fun main() = runBlocking { // this: CoroutineScope
GlobalScope.launch { // launch a new coroutine and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello") // main coroutine continues while a previous one is delayed
}
fun main() = runBlocking { // this: CoroutineScope
GlobalScope.launch { // launch a new coroutine and continue
delay(3000L)
println("World!")
}
println("Hello")
delay(2000L)
}
join()
이다fun main() = runBlocking { // this: CoroutineScope
val job = GlobalScope.launch {
delay(1000L)
println("World!")
}
println("Hello")
job.join()
}
fun main() = runBlocking { // this: CoroutineScope
val job = GlobalScope.launch {
delay(1000L)
println("World!")
}
val job2 = GlobalScope.launch {
delay(1000L)
println("World!")
}
val job3 = GlobalScope.launch {
delay(1000L)
println("World!")
}
println("Hello")
job.join()
job2.join()
job3.join()
}
Structured concurrency
이란 패러다임을 사용한다fun main() = runBlocking {
this.launch {
delay(1000L)
println("World!")
}
this.launch {
delay(1000L)
println("World!")
}
this.println("Hello")
}
fun main() = runBlocking { // this: CoroutineScope
launch {
doWorld()
}
println("Hello")
}
// this is your first suspending function
suspend fun doWorld() {
doBeautiful()
delay(1000L)
println("World!")
}
suspend fun doBeautiful() {
println("beautiful")
}
fun main() = runBlocking {
repeat(100_000) { // launch a lot of coroutines
launch {
delay(5000L)
print(".")
}
}
}
fun main() = runBlocking { // this: CoroutineScope
launch {
repeat(5) {
println("Coroutine A, $it")
}
}
launch {
repeat(5) {
println("Coroutine B, $it")
}
}
println("Coroutine Outer")
}
//어느 thread에서 찍히는지 보려고 println 오버라이드
fun <T> println(msg: T) {
kotlin.io.println("$msg [${Thread.currentThread().name}]")
}
/*
Coroutine Outer [main]
Coroutine A, 0 [main]
Coroutine A, 1 [main]
Coroutine A, 2 [main]
Coroutine A, 3 [main]
Coroutine A, 4 [main]
Coroutine B, 0 [main]
Coroutine B, 1 [main]
Coroutine B, 2 [main]
Coroutine B, 3 [main]
Coroutine B, 4 [main]*/
fun main() = runBlocking { // this: CoroutineScope
launch {
repeat(5) {
println("Coroutine A, $it")
delay(10L)
}
}
launch {
repeat(5) {
println("Coroutine B, $it")
}
}
println("Coroutine Outer")
}
/*Coroutine Outer [main]
Coroutine A, 0 [main]
Coroutine B, 0 [main]
Coroutine B, 1 [main]
Coroutine B, 2 [main]
Coroutine B, 3 [main]
Coroutine B, 4 [main]
Coroutine A, 1 [main]
Coroutine A, 2 [main]
Coroutine A, 3 [main]
Coroutine A, 4 [main]*/
fun main() = runBlocking { // this: CoroutineScope
launch {
repeat(5) {
println("Coroutine A, $it")
delay(10L)
}
}
launch {
repeat(5) {
println("Coroutine B, $it")
delay(10L)
}
}
println("Coroutine Outer")
}
/*Coroutine Outer [main]
Coroutine A, 0 [main]
Coroutine B, 0 [main]
Coroutine A, 1 [main]
Coroutine B, 1 [main]
Coroutine A, 2 [main]
Coroutine B, 2 [main]
Coroutine A, 3 [main]
Coroutine B, 3 [main]
Coroutine A, 4 [main]
Coroutine B, 4 [main]*/
- Job
- cancle()
- Cancellation is cooperative
- way1 : suspend를 주기적으로 호출
- way2 : isActive로 상태 체크
- Timeout
- withTimeout
- withTimeoutOrNull
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("job: I'm sleeping ${i++} ...")
delay(500L)
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancel()// cancels the job and waits for its completion
job.join()
//위 두 함수를 동시에 실행하는 job.cancelAndJoin() 으로 해도 됨
println("main: Now I can quit.")
}
~방법 1: 주기적으로 suspend function을 호출~
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
try {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // computation loop, just wastes CPU
// print a message twice a second
if (System.currentTimeMillis() >= nextPrintTime) {
// delay(1L)
yield() //suspend 함수를 하나라도 실행해야함!!
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}catch (e:Exception){
println("Exception: $e")
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
}
/*
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
Exception: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@4f7b794d
main: Now I can quit.*/
~방법2: isActive 상태가 아니면 코루틴을 종료~
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // cancellable computation loop
// print a message twice a second
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
}
/*job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.*/
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} finally {
//코루틴이 예외를 던지면 finally블록 안 로직을 실행하고 종료된다
//따라서 리소스를 해제하는 로직은 여기서 작성한다
println("job: I'm running finally")
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
}
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} finally {
withContext(NonCancellable) {
println("job: I'm running finally")
delay(1000L)
println("job: And I've just delayed for 1 sec because I'm non-cancellable")
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
}
fun main() = runBlocking {
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
}
fun main() = runBlocking {
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
"Done" // will get cancelled before it produces this result
}
println("Result is $result")
}
- Async to sequential
- Sequential by default
- The Dream Code on Android
- async
- Concurrent using async
- Lazily started async
- Structred concurrency
- Async-style function
- Structred concurrency with async
fun main() = runBlocking {
val time = measureTimeMillis {
val one = doSomethingOne()
val two = doSomethingTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
}
suspend fun doSomethingOne(): Int {
println("doSomethingOne")
delay(1000L)
//서버통신같은 heavy한 코드 즉, 비동기로 처리되어야하는 동작!
return 13
}
suspend fun doSomethingTwo(): Int {
println("doSomethingTwo")
delay(1000L)
//서버통신같은 heavy한 코드 즉, 비동기로 처리되어야하는 동작!
return 29
}
/*doSomethingOne
doSomethingTwo
The answer is 42
Completed in 2013 ms*/
suspend function 을 어떻게 조합해서 코루틴을 유용하게 사용할 수 있을까
retrofit 호출 같은 것(=heavy 한 비동기 job)을 순차적으로 실행하고 싶으면 어떻게 해야되는가?
코루틴에서는 일반 코드처럼 작성하면, 비동기일지라도 순차적으로 실행되는 것이 기본이다.
즉 비동기 실행을 순차적으로 맞춰줄 수 있고, 콜백을 순차 실행한다.(와우..코루틴 없이 콜백 지옥으로 retrofit처리했을때는 끝나는 순서가 제각각이라 별도의 체크가 필요했는데..!)
위의 예시에서 function 2개가 거의 동시에 실행되어서 콜백을 받을때 어느게 먼저 끝날 지 몰라서 비동기 처리하기 어려웠었음 근데 코루틴을 알아서 비동기처리가 순차실행되니까 One끝나고 Two실행됨
이건 안드로이드에서 말하는 dream Code의 시작점이 되는건데, 일반 코드 적듯 순차적으로 비동기 처리를 적기만 하면 알아서 순차처리되어서 싱크도 딱딱 맞는다
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initUi()
// case1
button_coroutines.setOnClickListener {
CoroutineScope(Dispatchers.Main).launch { // 메인 스레드를 블로킹하지 않음
runDreamCode() // Ui 업데이트 됨
}
}
// case2
button_blocking.setOnClickListener {
runBlocking { // 메인 스레드를 블로킹한다.
runDreamCode() // Ui 업데이트 안됨
}
}
}
suspend fun runDreamCode() {
val time = measureTimeMillis {
val one = doSomethingUsefuleOne() // CPU를 많이 먹는 동작1
val two = doSomethingUsefuleTwo() // CPU를 많이 먹는 동작2
println("The answer is ${one + two}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefuleOne(): Int {
println("doSomethingUsefuleOne")
delay(1000L)
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L)
return 29
}
}
case1 ) 코루틴 안에서 일반 코드처럼 작성하였고, 심지어 메인 스레드에서 실행되었지만, UI를 블로킹하지 않는다. UI는 UI대로 그려지고 있고 코루틴 안에서 일어나는 일들은 다른 스레드에서 실행되는 것처럼 순차적으로 실행된다.
case2 ) 코루틴이 아닌 형태로, runBlocking을 활용하여 명시적으로 메인 스레드를 블로킹하도록 한다면 어떻게 될까? -> 모든 UI가 다 멈춘다.
fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
return 29
}
만약 첫번째와 두번째 연산이 dependency가 없다면=독립적으로 일어나도 되는 일이라면, 굳이 순차적으로 실행하지 않고, 비동기적으로 실행하고 싶을 것이다
이럴경우 항상 async 이용해서 명시적으로 콜해야한다
아까 코드는 one을 실행하고 one이 suspend니까 기다릴 후 two가 실행되었다면 이번 코드는 async코드블럭 실행후 안기다리고 바로 다음 async를 실행한다
await()는 서로의 결과를 기다린 후 더해야하니까 사용한다
이런식으로 사용하면 비동기를 순차실행, 동시실행 등 자유자재로 코루틴을 사용할 수 있다.
async 는 Job을 상속한 객체를 반환한다.Job을 상속받았으니까 await(),cancle()같은 function도 사용할 수 있는거고, one.await() 하면 job이 끝날때까지 기다린다.
fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
// some computation
one.start() // start the first one
two.start() // start the second one
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
return 29
}
/ note that we don't have `runBlocking` to the right of `main` in this example
fun main() {
val time = measureTimeMillis {
// we can initiate async actions outside of a coroutine
val one = somethingUsefulOneAsync()
val two = somethingUsefulTwoAsync()
// but waiting for a result must involve either suspending or blocking.
// here we use `runBlocking { ... }` to block the main thread while waiting for the result
runBlocking {
println("The answer is ${one.await() + two.await()}")
}
}
println("Completed in $time ms")
}
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulOneAsync() = GlobalScope.async {
doSomethingUsefulOne()
}
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulTwoAsync() = GlobalScope.async {
doSomethingUsefulTwo()
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
return 29
}
fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
println("The answer is ${concurrentSum()}")
}
println("Completed in $time ms")
}
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
fun main() = runBlocking<Unit> {
try {
failedConcurrentSum()
} catch(e: ArithmeticException) {
println("Computation failed with ArithmeticException")
}
}
suspend fun failedConcurrentSum(): Int = coroutineScope {
val one = async<Int> {
try {
delay(Long.MAX_VALUE) // Emulates very long computation
42
} finally {
println("First child was cancelled")
}
}
val two = async<Int> {
println("Second child throws an exception")
throw ArithmeticException()
}
one.await() + two.await()
}
어떤 코루틴에서 exception이 발생했을 때, 코루틴이 cancel되면, hierarchy로 전파가 된다. 전파가 되면 다 종료가 된다.
coroutineScope 안에서 async 코루틴 빌더가 2개 동시적으로 실행되고 있고 어떤 async 블록에서 ArithmeticException 를 던지면 해당 async가 포함된 coroutineScope를 불러낸 부모 코루틴인 runBlocking에 취소가 전파되어서 예외를 catch하게됨
이런 실행 취소 전파가 의미하는건 실행 중 연관되어있던 코루틴 중 예외가 발생하면 다른 코루틴 모두 예외를 전파받고, 정상적으로 리소스를 뱉어낸 후 종료죌 수 있다는 것
어떻게 내가 순차적으로 작성한 코드가 비동기도 되고 콜백도 되고 하는거지?
어떻게 코루틴에서 중단되었다가 재개될 수 있는거지?
코틀린이 내부적으로 어떻게 동작하는 걸까? 마법은 없다..
Continuation-Passing Style = CPS
내부적으로, 콜백같은걸 계속 넘겨주면서 콜백 콜백 되는거다..
kotlin suspending function
CPS transformation
JVM에 들어갈 때는, 우리가 호출한 함수에 Continuation cont 가 인수에 생긴다.
코루틴 스코프에서 모든 코루틴들을 실행하도록 하고, 화면을 나가면, 코루틴 스코프에 cancel을 해주면 모든 job들이 취소가 된다.
안드로이드에서, 라이프사이클이 있는 클래스인 경우, 라이프사이클에 맞게 코루틴 스코프를 연결해놓고 실행시키면 된다.
fun main() = runBlocking<Unit> {
launch { // context of the parent, main runBlocking coroutine
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher
println("Default : I'm working in thread ${Thread.currentThread().name}")
}
newSingleThreadContext("MyOwnThread").use { // will get its own new thread
launch(it) {
println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}
}
fun main() {
newSingleThreadContext("Ctx1").use { ctx1 ->
newSingleThreadContext("Ctx2").use { ctx2 ->
runBlocking(ctx1) {
log("Started in ctx1")
withContext(ctx2) {
log("Working in ctx2")
}
log("Back to ctx1")
}
}
}
}
fun main() = runBlocking<Unit> {
// launch a coroutine to process some kind of incoming request
val request = launch {
// it spawns two other jobs
GlobalScope.launch(Job()) {
println("job1: I run in my own Job and execute independently!")
delay(1000)
println("job1: I am not affected by cancellation of the request")
}
// and the other inherits the parent context
launch {
delay(100)
println("job2: I am a child of the request coroutine")
delay(1000)
println("job2: I will not execute this line if my parent request is cancelled")
}
}
delay(500)
request.cancel() // cancel processing of the request
delay(1000) // delay a second to see what happens
println("main: Who has survived request cancellation?")
}
/*
job1: I run in my own Job and execute independently!
job2: I am a child of the request coroutine
job1: I am not affected by cancellation of the request
main: Who has survived request cancellation?*/
fun main() = runBlocking<Unit> {
// launch a coroutine to process some kind of incoming request
val request = launch {
repeat(3) { i -> // launch a few children jobs
launch {
delay((i + 1) * 200L) // variable delay 200ms, 400ms, 600ms
println("Coroutine $i is done")
}
}
println("request: I'm done and I don't explicitly join my children that are still active")
}
println("Now processing of the request is complete")
}
/*Now processing of the request is complete
request: I'm done and I don't explicitly join my children that are still active
Coroutine 0 is done
Coroutine 1 is done
Coroutine 2 is done*/
fun main() = runBlocking<Unit> {
// launch a coroutine to process some kind of incoming request
val request = CoroutineScope(Dispatchers.Default).launch {
repeat(3) { i -> // launch a few children jobs
launch {
delay((i + 1) * 200L) // variable delay 200ms, 400ms, 600ms
println("Coroutine $i is done")
}
}
println("request: I'm done and I don't explicitly join my children that are still active")
}
println("Now processing of the request is complete")
}
//Now processing of the request is complete
class Activity {
private val mainScope = CoroutineScope(Dispatchers.Default) // use Default for test purposes
fun destroy() {
mainScope.cancel()
}
fun doSomething() {
// launch ten coroutines for a demo, each working for a different time
repeat(10) { i ->
mainScope.launch {
delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
println("Coroutine $i is done")
}
}
}
} // class Activity ends
fun main() = runBlocking<Unit> {
val activity = Activity()
activity.doSomething() // run test function
println("Launched coroutines")
delay(500L) // delay for half a second
println("Destroying activity!")
activity.destroy() // cancels all coroutines
delay(1000) // visually confirm that they don't work
}