[안드로이드스튜디오_문화][Coroutine]

기말 지하기포·2023년 11월 20일
0

#코루틴 학습 링크 1 : https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md
#코루틴 학습 링크 2 : https://kotlinlang.org/docs/coroutines-guide.html#additional-references

Coroutine 코드

-스쿱 내부의 코드들은 코루틴 코드 / 스쿱 내부의 코드들은 코루틴 코드(x)

Suspending Function

-코루틴 코드의 일부를 함수로 분리하고 싶을 때에는 suspend fun 키워드를 붙히면 된다.

-코루틴 내부의 코드를 별도의 함수로 분리하고자 할 때, 중요한 점은 분리된 함수가 코루틴의 일시 중단(suspension)을 지원해야 하며 반드시 코루틴 내에서 호출되는 함수는 suspend 키워드를 사용하여 선언되어야 합니다.

-suspend 함수는 코루틴 내부 또는 다른 suspend 함수 내에서만 호출될 수 있다.

일반함수를 코루틴내부에서 사용하기

-위해서는 CoroutineScope를 제공해주는 Suspend Function들을 활용해서 만들어 주면된다.

-아래에서 설명하는 것들은 모두 suspendFunction으로서 suspendFunction을 만들고자 할 때 사용 할 수 있는 것들이다. 즉, suspendFunction 내부에서 다른 코루틴을 만들고 실행하려면 아래와 같은 suspendFunction을 사용하면 된다.

-coroutineScope : coroutineScope{}를 호출한 코루틴의 컨텍스트를 유지한채로 코루틴 스쿱을 만든다. 또한, runBlocking과는 호출한 thread를 멈추게 하지 않는다.

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = ScopeCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}

-supervisorScope

public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = SupervisorCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}

-아래 코드들 예시코든 아래에 있음.

-withContext() : ()에서 Context 설정을 할 수 있다.

-withTimeout() : 일정 시간이 지나면 코루틴 종료(TimeoutCancellationException 발생)

-withTimeoutOrNull() : 일정 시간이 지나면 코루틴 종료(null 반환)

코루틴은 경량 스레드라고 불리기도 한다.

-코루틴은 스레드에서만 실행되는데 하나의 스레드 안에 여러개의 코루틴이 있어서 스레드에서 코루틴을 다루게 된다. 즉, 하나의 스레드에 여러개의 코루틴이 있기 때문에 경량 스레드라고 불리는 것이다.

-그러나 한번에 하나의 코루틴만 실행 될 수 있다. 이는 코루틴이 비동기적으로 실행 될 수 있는 것이지 반드시 동시에 짜라락하고 모든 코루틴이 함께 실행되는 것은 아니기 때문이다. 즉, 하나의 스레드에서 실제로 실행되는 코루틴은 단 하나이고, 나머지는 suspend function을 사용해서 대기상태에 넣어둔 후 에 스레드에서 대기중인 코루틴을 선택하면서 서로 번갈아 가면서 실행 시키는 것이다.

-하지만, 코루틴이 다른 스레드로 분산된다면 각각의 스레드에서 코루틴을 실행시키기 때문에 동시에 실행 할 수 있기도 한다.

  • 예시코드(1)
// Coroutine은 상호 협력한다.
// delay가 있을 때마다 다른 Coroutine에게 Thread를 양보한다.
// Coroutine은 가벼워서 10만개까지 쌉가능이야.
suspend fun doOneTwoThree2() = coroutineScope {
    val job = launch {
        println("launch1: ${Thread.currentThread().name}")
        delay(1000L)
        println("3!")
    }
    job.join()
    launch {
        println("launch2: ${Thread.currentThread().name}")
        println("1!")
    }

    repeat(100_000) {
        // repeat()을 통해서 launch{}를 이용한 CoroutineScope가 100,000만개 만드는데
        // delay(500L)이 있으므로 , 500ms 동안 println("launch3: ${Thread.currentThread().name}")이
        // 100_000만회 실행되고 500ms가 지나면 println("2!")가 100_000만회 실행된다.
        launch {
            println("launch3: ${Thread.currentThread().name}")
            delay(500L)
            println("2!")
        }
    }
    println("4!")
}
fun main() = runBlocking{
    doOneTwoThree2()
}

StructuredConcurrency

-parent가 어떤 이유로든 취소되면, parent의 모든 children이 취소된다.

-child에서 exception이 던져져서 취소되면, exception은 parent로 전파되어서 parent를 취소시킨다. child가 명시적인 취소로 인해 취소되면 parent로 취소가 전파되지 않는다.

  • 예시코드(1)
  • 아래 코드는 계층적인 코루틴의 예시이다.
  • 상위 코루틴이 하위 코루틴이 끝날때까지 기다려서 5초 이후에 최상위 코루틴이 종료된다.
fun main() = runBlocking {
    val elapsed = measureTimeMillis {
        val job = launch {
            launch {
                println("launch1: ${Thread.currentThread().name}")
                delay(5000L)
            }
            launch {
                println("launch2: ${Thread.currentThread().name}")
                delay(10L)
            }
        }
        job.join()
    }
    println(elapsed) 
}

CoroutineScope Builder

RunBlocking(동기성)

-runBlocking은 runBlocking을 호출한 Thread를 차단한 채로 코루틴 스코프를 생성한다. 해당 Thread는 코루틴의 진행이 완료 될때까지 차단된다. 즉, runBlocking이 생성한 CoroutineScope 내부의 코드 실행을 보장한다. 따라서 MainThread에서 runBlocking을 사용할 때 주의해야 한다.

-runBlocking{}은 suspend function을 포함하는 순차적인 코드를 작성하고자 할 때 또는 테스트 할 경우에 주로 사용한다.

-만약 MainThread에서 runBlocking을 사용했을 때는 ANR 에러가 발생할 수 도 있으므로 주의해야 한다.

  • 예시코드(1)
fun main() = runBlocking {
    // 'Thread.currentThread().name'을 활용해서 현재 사용되고 있는 코루틴의 이름을 알 수 있다.
    println(Thread.currentThread().name)
    println("Hello")
}

// runBlocking은 자신의 코드 블록이 종료되기 전까지 다른 Coroutine이 실행되는 것을 막는다.
  • 예시코드(2)
fun main() = runBlocking {
    println(coroutineContext)
    println(Thread.currentThread().name)
    println("Hello")
}
  • 예시코드(3)
fun main() = runBlocking{
    // runBlocking의 수신객체는 CoroutineScope이다. 따라서 코드 블록 내부에서 코루틴에 있는 모든 기능들을
    // 사용 할 수 있다.
    // ** 수신객체 : 'extension lambda'라고 불리는데 , 이는 람다를 확장한 것 처럼 사용 할 수 있기 때문이다.
    // 즉 , runBlocking의 코드 블록이 코루틴을 확장한 것 처럼 사용 할 수 있다는 의미야.
    println(this)
    println(Thread.currentThread().name)
    println("Hello")
}

// println(this)의 결과값으로 BlockingCoroutine{Active}라고 나올 텐데 이는 , BlockingCoroutine이 활성상태라는 것을 의미한다.
// BlockingCoroutine : runBlocking{}으로 부터 생성된 CoroutineScope의 객체이다.
// - Coroutine은 CoroutineScope 내부에서만 사용이 가능하다.

launch(기본은 비동기성 , Dispatchers 영향 O)

-launch{}는 runBlocking과는 다르게 단독으로는 CoroutineScope를 구성 할 수 없으며, 오직 코루틴 스코프 내부에서만 사용이 가능하다.

-launch{}는 가능하다면 다른 코루틴과 병렬적으로 실행이 가능하며 , launch{}의 코드블록은 바로 실행되는 것이 아니라 실행될 준비가 완료되면 실행된다.

-launch(start = CoroutineStart.LAZY)를 사용하면 시작을 늦출 수 있다. 사용방법은 async(start = CoroutineStart.LAZY)와 동일하다.
-Job 객체를 반환한다. Job에 대한 설명은 아래 스크롤 내리다 보면 있다.

  • 예시코드(1)
fun main() = runBlocking {
    launch {
        println("launch : ${Thread.currentThread().name}")
        println("World!")
    }
    println("runBlocking : ${Thread.currentThread().name}")
    println("Hello")
}

(1) launch{}는 runBlocking과 다르게 단독으로는 CoroutineScope를 구성 할 수 없다. 
->launch는 코루틴 내에서만 사용이 가능하다.

(2) launch{}의 CoroutineScope : 가능하다면 다른 코루틴 스코프와 병렬적으로 실행이 가능하다.

(3) 위 코드 실행 결과 해석
-runBlocking이 MainThread의 실행을 차단한 채로 해당 CoroutineScope 내부 코드가 진행된다.
-println()이 순차적으로 실행되는 동시에 launch{} 블록은 새로운 코루틴을 생성하여 비동기적으로 실행을 
 시작한다.
  • 예시코드(2)
    -delay()는 코루틴 스코프에 일시 중단 명령을 내리는 중단함수로서 parameter로 받은 숫자ms만큼 코루틴이 중단된다.

    -delay()는 Thread를 차단하지 않으며, 중단된 시간동안 코루틴의 실행이 중단된 다음
    지정된 시간이 지나면, 코루틴의 실행을 재개한다.

    -delay()에 음수 또는 0이 parameter로 주어진다면 CancellationException을 발생시킨 후 코루틴이 즉시 재개된다. (:delay가 취소 가능한 suspend function이기 때문에)

    -Thread.sleep()은 코루틴의 실행을 중단시키지 않는다.
fun main() = runBlocking {
    launch {
        println("launch : ${Thread.currentThread().name}")
        println("delay1")
        delay(450L)
        launch {
//            delay(500L)
            println("delay2")
        }
    }
    println("runBlocking : ${Thread.currentThread().name}")
    delay(200L)
    println("delay3")
}
  • 예시코드(3)
fun main() = runBlocking {
    launch {
        println("launch1 : ${Thread.currentThread().name}")
        delay(1000L) // suspension point
        println("3! ${Thread.currentThread().name}")
    }
    launch {
        println("launch2 : ${Thread.currentThread().name}")
        println("1! ${Thread.currentThread().name}")
    }
    println("runBlocking : ${Thread.currentThread().name}")
    delay(500L) // suspension point
    println("2! ${Thread.currentThread().name}")
}
// 코루틴은 단일 Thread를 사용하는 경우에도, 서로 양보하면서 실행되기 때문에 매우 유용하다.

> 위코드가 실행되면 MainThread가 차단된 채로 runBlocking의 내부 코드 블락의 실행이 순차적으로 
  실행되는데 , println()이 첫번째로 실행되는 동안에 동시에 위의 launch 블락 두개가 비동기적으로 
  진행되는데, runBlocking은 동기성을 가져서 조금 빠르게 위에 있는 launch 부터 새로운 코루틴을 
  만들기 때문에 
  runBlocking: @coroutine#1
  launch1: @coroutine#2
  launch2: @coroutine#1 이렇게 출력된다.
  • 예시코드(4)
fun main() {
    runBlocking { 
        launch {
            println("launch1 : ${Thread.currentThread().name}")
            delay(1000L) // main()을 차단하지 않은 상태로 코루틴을 1초동안 지연시킨다.
            println("3!")
        }
        launch {
            println("launch2 : ${Thread.currentThread().name}")
            println("1!")
        }
        println("runBlocking : ${Thread.currentThread().name}")
        delay(500L) // main()을 차단하지 않은 상태로 코루틴을 1초동안 지연시킨다.
        println("2!")
    }
    println("4!")
}

// 위 코드는 runBlocking 안에 두개의 launch가 속한채 계층화 되어있는 구조입니다.
// runBlocking은 자신 안에 포함된 두개의 launch{}가 종료되기 전까지 종료되지 않습니다.
  • 예시코드(5)
suspend fun doThree() {
    println("launch3 : ${Thread.currentThread().name}")
    delay(1000L)
    println("3!")
}

fun doOne() {
    println("launch1: ${Thread.currentThread().name}")
    println("1!")
}

suspend fun doTwo() {
    println("runBlocking: ${Thread.currentThread().name}")
    delay(5000L)
    println("2!")
}

fun main() = runBlocking {
    launch { doThree() }
    launch { doOne() }
    doTwo()

}

> 위 코드를 실행하면 최종 결과가 출력되기 전까지 5초가 조금 넘는다. 이는 doTwo()의 delay(500)
  때문이다.
> 중요한 점은 doTwo(){}의 delay(5000L)이 launch{} 두개의 실행에 영향을 미치지 않는다는것이다.
  이는 delay(500)이 해당 코루틴에는 영향을 주지만 다른 독립적인 코루틴의 실행에는 영향을 못 끼치기
  때문이다 + launch는 비동기적으로 실행된다.

async(기본은 비동기성 , Dispatchers 영향 O)

-async는 launch와 공통점이 많다. Coroutine Scope 내부에서만 async를 사용해서 Coroutine Scope를 만들 수 있고 , 비동기성을 띤다는 점이 있다.

-하지만 async는 Job을 상속받은 Deffered를 반환하고(Job을 상속받았기 때문에 Job에서 사용할 수 있는 Method 사용 가능하다.), await()이라는 함수를 사용해서 Deffered 객체가 반환하는 값을 받아 올 수 있다.

-await()은 suspend function이고, async{} 블록의 수행이 종료되었는지 확인하고 끝났다면 반환값을 받아오고 , 종료되지 않았다면 현재 코루틴이 suspend하다가 반환 값이 나오면 받아온다.

-즉, CoroutineScope에서 반환하는 값을 받아오는 Coroutine을 만들고 싶다면, 'async'를 사용해서 async.await()을 사용하면되고 CoroutineScope에서 반환하는 값을 받을 필요가 없거나 반환값이 없다면 'launch'를 사용하면 된다.

-async를 호출한 순간 async는 코루틴을 생성할 준비를 하고 생성이 완료되면 코루틴을 실행한다. 그렇다면 async가 코루틴을 생성할 준비를 하는 것을 명시적으로 지정해주고 싶다면 어떻게 하면 될까? 이럴 때 사용하는 것이 async(satart = CoroutineStart.LAZY)이다. 이것을 사용하면 명시적으로 .start() 메서드를 사용해서 시작을 시켜줘야지 async가 코루틴을 생성할 준비를 시작하고 생성이 완료되면 코루틴을 시작한다. 또한 .start()가 시작되지 않았는데 .await()을 만나게 되면, async가 코루틴을 생성할 준비를 시작하고 , 코루틴을 생성한다.

  • 예시코드(1)
suspend fun getRandom1_2() : Int {
    delay(1000L)
    return Random.nextInt(0 , 500)
}

suspend fun getRandom2_2() : Int {
    delay(1000L)
    return Random.nextInt(0 , 500)
}

fun main() = runBlocking{
    val elapsedTime = measureTimeMillis{
        // async를 사용한 이유는 getRandom1_2()를 호출하기 위한게 아니라 , 
        // 호출된 Coroutine의 결과값을 가져오기 위해서 사용한거야.
        val value1 = async { getRandom1_2() }
        val value2 = async { getRandom2_2() }
        // .await()은 job.join의 기능에 결과를 가져오는 기능이 추가 되었다고 생각하면 된다.
        // .await()도 suspension point이다.
        println("${value1.await()} + ${value2.await()} = ${value1.await() + value2.await()}")
    }
    println(elapsedTime)
}
  • 예시코드(2)
suspend fun getRandom1_3() : Int {
    delay(1000L)
    return Random.nextInt(0 , 500)
}

suspend fun getRandom2_3() : Int {
    delay(1000L)
    return Random.nextInt(0 , 500)
}

fun main() = runBlocking {
    val elapsedTime = measureTimeMillis {
        val value1 = async ( start = CoroutineStart.LAZY ) {
            getRandom1_3()
        }
        val value2 = async ( start = CoroutineStart.LAZY ) {
            getRandom2_3()
        }
        value1.start()
        value2.start() // .start()를 주석 처리하면 .await()이 호출되는 시점에 value2가 실행된다.
        println("${value1.await()} + ${value2.await()} = ${value1.await() + value2.await()}")
    }
    println(elapsedTime)
}

> async를 호출하는 순간 async로부터 생성된 Coroutine Scope의 내부코드들은 실행을 예약한다.
> 그렇다면 조금 늦게 실행되게 하고 싶으면 어떻게 해야 할까
> 그럴때 사용하는 것이 async(start = CoroutineStart.LAZY)이다. 실행 예약을 하고 싶으면 
 .start() 메서드를 사용하면 된다.
  • 예시코드(3)
    -아래 코드는 Structured concurrency과 async()를 사용한 예시 코드이다.
suspend fun getRandom1_4() : Int {
    try {
        delay(1000L)
        return Random.nextInt(0 , 500)
    } finally {
        println("getRandom1 is cancelled")
    }
}
suspend fun getRandom2_4() : Int {
    delay(500L)
    throw IllegalStateException()
}

suspend fun doSomething() = coroutineScope {
// 부모 코루틴 : 자식 코루틴이 취소되면 부모 코루틴도 취소된다.
    val value1 = async {
    // 자식 코루틴 1 : 자식 코루틴 2에서 에러가 발생 했으니까 Coroutine을 취소 하라고 알려준다.
                                       
    // 왜냐면 자식 코루틴 2와 자식 코루틴 1은 형제 코루틴이기 때문이다.
        getRandom1_4()
    }
    val value2 = async { getRandom2_4() } // 자식 코루틴 2 : 에러 발생
    try {
        println("${value1.await()} + ${value2.await()} = ${value1.await() + value2.await()}")
    } finally {
        println("doSomething is cancelled.")
    }
}

fun main() = runBlocking {
    try {
        doSomething()
    } catch (e:IllegalStateException) {
        println("doSomething failed : $e")
    }
}

CoroutineScope 종류

GlobalScope

-현재는 DelicateCoroutinesApi 상태로 삭제된 API라는 뜻이다.

-어떤 계층에도 속하지 않고 영원히 동작하게 된다는 문제점이 있어서 삭제되었고 현재는 Coroutine Scope만 남아있다.

CoroutineScope

-CoroutineScope는 parameter로 CoroutineContext를 받고(Coroutine Element 하나만 넣어도 된다.), 우리는 이 CoroutineScope를 활용해서 코루틴 스코프를 만들어 낸다.

  • 예시코드(1)
suspend fun printRandom2() {
    delay(500L)
    println(Random.nextInt(0 , 500))
}

@OptIn(ExperimentalStdlibApi::class)
fun main() {
    val scope = CoroutineScope(Dispatchers.Default + CoroutineName("Scope"))
    // CoroutineScope의 parameter에 이런식으로 CoroutineContext 넣어서 CoroutineScope를 만든다.
    val job = scope.launch(Dispatchers.IO) {
    // 이처럼 launch를 활용해서 Dispatchers 변경 쌉가능
        launch { printRandom2() }
        println(coroutineContext[CoroutineDispatcher])
        println(coroutineContext[CoroutineName])
    }
    Thread.sleep(1000L)
    // delay()는 coroutine 내부 또는 suspend function 내부에서만 사용 할 수 있어서 사용한거야.
}

Job && Cancel && TimeOut

Job

-launch{}로부터 반환되는 값으로 코루틴의 작업을 관리와 수명주기 관리를하는 역할을 수행 할 수 있다.

-Job과 관련된 메서드 및 프로퍼티는 다음과 같다.

  • Job.join() : Job 작업이 완료될 때까지 join을 호출한 코루틴을 일시 중단 시킨다.
  • Job.joinAll(여기에 Job 객체들 넣어줘) : 모든 job 들이 종료될 때까지 대기
  • Job.cancel() : 코루틴을 취소한다.
  • Job.cancelAndJoin() :
  • isActive : 코루틴의 활상상태 반환(true : 활성O , false : 활성X)
  • isCancelled : 코루틴의 취소상태 반환(true : 취소O , false : 취소X)
  • isCompleted : 코루틴의 완료여부 반환(true : 완료O , false : 완료X)

-이처럼 Job과 관련된 메서드 및 프로퍼티를 활용하면 효율적으로 코루틴을 사용 할 수 있다.

  • 예시코드(1)
  • 아래코드는 job.join을 활요한 코드이다. job.join()이 호출되면 job의 작업이 완전히 종료될때까지 launch2와 launch3은 실행되지 못하고 (즉, job.join()을 호출한 코루틴이 일시 중단되기 때문이야 하지만, 스레드를 차단하는 것은 아니다.) job의 작업이 완료되야만 launch2와 launch3이 실행된다.
  • 또한 doOneTwoThree가 suspendFunction이기 때문에 doOneTwoThree의 작업이 완료되기 전까지는 runBlocking의 아래 두개 코드가 실행되지 않는다.
suspend fun doOneTwoThree() = coroutineScope {
    val job = launch {
        println("launch1: ${Thread.currentThread().name}")
        delay(1000L)
        println("3!")
    }
    job.join() // job이 종료될때까지 CoroutineScope는 중단된다.
    launch {
        println("launch2: ${Thread.currentThread().name}")
        println("1!")
    }
    launch {
        println("launch3: ${Thread.currentThread().name}")
        delay(500L)
        println("2!")
    }
    println("4! : ${Thread.currentThread().name}")
}

fun main() = runBlocking {
    doOneTwoThree()
    println("runBlocking: ${Thread.currentThread().name}")
    print("5!")
}

// 위 코드를 아래 코드로 조금 수정 해보면 아래처럼 되는데 이렇게 하면 job의 작업이 완료되면
println("4! : ${Thread.currentThread().name}")
println("runBlocking: ${Thread.currentThread().name}")
println("5!") 가  실행되는 것을 알 수 있는데, 이는 기존에 doOneTwoThree()는 suspendFunction
이였던 것이 suspendFunction이 아니게 바뀌었기 때문이다.

fun main() = runBlocking {
    val job = launch {
        println("launch1: ${Thread.currentThread().name}")
        delay(1000L)
        println("3!")
    }
    job.join() // job이 종료될때까지 CoroutineScope는 중단된다.
    launch {
        println("launch2: ${Thread.currentThread().name}")
        println("1!")
    }
    launch {
        println("launch3: ${Thread.currentThread().name}")
        delay(500L)
        println("2!")
    }
    println("4! : ${Thread.currentThread().name}")
    println("runBlocking: ${Thread.currentThread().name}")
    println("5!")
}
  • 예시코드(2)
suspend fun doOneTwoThree3() = coroutineScope{
    val job1 = launch {
        println("launch1: ${Thread.currentThread().name}")
        delay(1000L)
        println("3!")
    }
    val job2 = launch {
        println("launch2: ${Thread.currentThread().name}")
        println("1!")
    }
    val job3 = launch {
        println("launch3: ${Thread.currentThread().name}")
        delay(500L)
        println("2!")
    }
    delay(800L)
    job1.cancel() // job1의 실행을 취소
    job2.cancel() // job2의 실행을 취소
    job3.cancel() // job3의 실행을 취소
    println("4!")
}

fun main() = runBlocking {
    doOneTwoThree3()
    println("runBlocking: ${Thread.currentThread().name}")
    println("5!")
}

Cancel(Cooperative하다)

-코루틴에서의 취소는 협동적으로 동작하기 때문에 코루틴을 취소하고 한다면 CoroutineScope 내부에서 실제로 취소 요청이 있었는지 없었는지를 확인해야 한다.

-따라서 코루틴을 완벽하게 취소하고 싶다면 [1]suspend function 사용 : 취소 시키려는 코루틴이 실행되는 동안은 스레드가 다른 코루틴을 실행시키려 자리 비울 수 있는 시간을 주어야 한다는 것이야. [2]명시적으로 isActivie 활용 이 두가지 방법을 활용해서 스레드에게 해당 코루틴이 취소되었다고 분명히 알려주어야 한다.

-코루틴이 취소되었을 때 만약 해당 코루틴에 할당된 리소스가 있다면 반드시 해제 해줘야한다. 코루틴이 .cancel()을 통해서 취소되면 CancellationException이 발생되기 때문에 , try~catch~finally 이렇게 대응을 해줘야 한다. catch에서 e를 잡아서 해당 코루틴에 할당된 자원을 해제해줘야 한다. 안전하게 해제하고 싶으면 finally 절에서 할당된 자원을 해제해주면 된다.

-특정 Coroutine은 취소가 불가능하기도 한데 이는 withContext(NonCancellable)을 활용해서 만들 수 있다.

  • 예시코드(1)
  • 아래 코드는 코루틴이 취소되지 않는다. 왜냐하면 job1내의 코드에 취소를 확인할 수 있는 방법이 존재하지 않기 때문이다.
suspend fun doCount4() = coroutineScope {
    // parameter로 아무것도 받지 않는 launch의 경우에는 MainThread에서 실행하게 되어있다.
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L
        while (i <=10) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
    delay(200L)
    job1.cancel()
    job1.join()
    println("doCount Done")
}
fun main() = runBlocking {
    doCount4()
}
  • 예시코드(2)
  • 하지만 아래와 같이 delay()를 넣어주면 job이 취소된다.
suspend fun doCount4() = coroutineScope {
    // parameter로 아무것도 받지 않는 launch의 경우에는 MainThread에서 실행하게 되어있다.
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L
        while (i <=10) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                delay(1000L)
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
    delay(2000L) // doCount4()의 실행을 중단 : job1은 영향 안 받고 계속 실행된다.
    job1.cancel()
    println("doCount Done")
}
fun main() = runBlocking {
    doCount4()
}
  • 예시코드(3)
> isActive : Coroutine은 this.isActive를 통해서 해당 Coroutine이 활성화 되어 있는지 확인 할 수 있다.
> isActivie는 Default 값이 true로 설정되어 있다.
suspend fun doCount6() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L
        while ( i <= 10 && isActive) { 
        // Coroutine이 활성화 되어 있는 상태에서만 while의 조건이 참이 되도록한거야.
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
    delay(300L)
    job1.cancel()
    println("doCount Done")
}
> 1출력 2출력 3출력 (이렇게 출력되는 동안 delay(300L)이 300L동안 실행 후 완료되면 job1.cancel()이
  실행 된 후 , isActive 값이 false로 변경되면 , job1이 취소된다.

fun main() = runBlocking {
    doCount6()
}
  • 예시코드(4)
  • 코루틴이 취소 될 때 할당된 자원을 해제하는 예시 코드
suspend fun doOneTwoThree7() = coroutineScope {
    val job1 = launch {
        try {
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("3!")
        } catch (e: Exception) {
            println("Error occurred: ${e.message}")
        } finally {
            println("job1 is finishing!")
            // finally 코드 블락에서 할당된 자원을 해제하는 코드를 작성해준다.
        }
    }
    val job2 = launch {
        try {
            println("launch2: ${Thread.currentThread().name}")
            delay(1000L)
            println("1!")
        } finally {
            println("job2 is finishing!")
            // finally 코드 블락에서 할당된 자원을 해제하는 코드를 작성해준다.
        }
    }
    delay(800L)
    job1.cancel()
    job2.cancel()
    println("4!")
}
fun main() = runBlocking {
    doOneTwoThree7()
}
  • 예시코드(5)
  • withContext(NonCancellable)을 통해서 만든 CoroutineScope는 취소되지 않는다. 참고로 withContext()는 suspend function이다.
suspend fun doOneTwoThree8() = coroutineScope {
    val job1 = launch {
        withContext(NonCancellable) {
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("1!")
        }
        delay(1000L)
        print("job1: end")
    }
    val job2 = launch {
        withContext(NonCancellable) {
            println("launch2: ${Thread.currentThread().name}")
            delay(1000L)
            println("2!")
        }
        delay(1000L)
        print("job2: end")

    }
    val job3 = launch {
        withContext(NonCancellable) {
            println("launch3: ${Thread.currentThread().name}")
            delay(1000L)
            println("3!")
        }
        delay(1000L)
        print("job3: end")
    }
    delay(800L)
    // job.cancel()이 호출된 시점에 , withContext(NonCancellable) {} 아래 코드는 모두 취소된다.
    job1.cancel()
    job2.cancel()
    job3.cancel()
    println("4!")
}
fun main() = runBlocking {
    doOneTwoThree8()
    println("runBlocking: ${Thread.currentThread().name}")
    println("5!")
}

TimeOut

-일정한 시간이 지난 후 Coroutine을 종료하고 싶을 때 사용한다.

-이렇게 만든 Coroutine이 일정한 시간이 지난 후 종료되면 TimeoutCancellationException이 발생한다.

-TimeoutCancellationException 없이 사용하고 싶으면 withTimeoutOrNull()을 사용하면 된다. 이는 일정한 시간이 지나면 null을 반환한다.

  • 예시코드(1)
suspend fun doCount9() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L

        while (i <= 10 && isActive) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
}

fun main() = runBlocking {
    withTimeout(500L) {
        doCount9()
    }
}
  • 예시코드(2)
// withTimeoutOrNull() { } : 해당 Coroutine이 종료 될 때 'null'을 반환한다.
suspend fun doCount10() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L

        while ( i<=10 && isActive) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
}

fun main() = runBlocking {
    val result = withTimeoutOrNull(500L) {
        doCount10()
        true
    } ?: false
    println(result)
}

CoroutineContext && Dispatchers

CoroutineContext

-CoroutineContext란, Coroutine의 작동 환경을 정의하고, 여러 CoroutineElement들을 조합하여 만들 수 있다. 즉, 여러 CoroutineElement 들을 조합해서 사용자가 원하는 Coroutine의 동작 방식을 구성 할 수 있다.

-CoroutineContext 요소 1 Coroutine Name : 코루틴의 이름 정해주기

-CoroutineContext 요소 2 Job : 코루틴 동작관리 + 코루틴 생명주기 관리

-CoroutineContext 요소 3 Dispatchers : 코루틴이 실행 될 스레드 관리

-CoroutineContext 요소 4 CEH : 코루틴에서 발생할 수 있는 예외 처리

CoroutineDispatchers

-Dispatcher.Main : 특정 스레드 풀에서 수행하지 않고 안드로이드 애플리케이션의 메인 스레드(UI Thread)에서 작업을 수행한다.

-Dispatcher.IO : 코어 수 보다 훨씬 많은 스레드를 가진 스레드 풀에서 수행한다. (IO 작업) -> CPU를 덜 소모하기 때문에.

-Dispatcher.Default : 코어 수에 비례하는 스레드를 가진 스레드 풀에서 수행한다. (복잡한 연산 같은 백그라운드 작업) -> 코어는 한 번에 하나의 일밖에 못하니까

-newSingleThreadContext(name : String) : 새로운 스레드 1개를 만들어서 수행한다. -> 무조건 Thread를 받아서 코루틴을 실행하려고 할 때

-newFixedThreadPoolContext(nThreads : Int , name : String) : nThreads 개수의 ThreadPool을 만들어서 name을 지어서 수행한다.

-Dispatcher.Unconfined : 시작은 부모 Thread에서 하지만 앞으로는 어디에서 수행 될 지 모른다.(문제야..비추)

CoroutineContext && Dispatchers 예시코드

  • 예시코드(1)
  • 아래코드는 CoroutineContext에 CoroutineDispatcher를 설정하는 예시코드다.
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking<Unit> {
    launch {
        // launch에 아무 Dispatchers도 넣지 않았을 경우에는 부모의 컨텍스트에서 실행이 된다.
        println("부모의 콘텍스트 / ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default){
        println("Default / ${Thread.currentThread().name}")
    }
    launch(Dispatchers.IO) {
        println("IO / ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) {
        // Unconfined : Main Thread에서 호출
        println("Unconfined / ${Thread.currentThread().name}")
    }
    launch(newSingleThreadContext("Geonhee Hwang")) {
        // newSingleThreadContext : 새로운 Thread를 만들어서 사용하라는 의미야.
        println("newSingleThreadContext / ${Thread.currentThread().name}")
    }
    launch(newFixedThreadPoolContext(12,"Twelve")) {
        println("newFixedThreadPoolContext / ${Thread.currentThread().name}")
    }
}
  • 예시코드(2)
  • 아래처럼 CoroutineContext를 Parameter로 받을 수 있다면, Dispatchers를 설정해 줄 수 있다.
  • 즉, launch / async / withContext .. 등에서 사용이 가능하다.
fun main() = runBlocking<Unit> {
    async {
        println("부모의 콘텍스트 / ${Thread.currentThread().name}")
    }
    async(Dispatchers.Default) {
        println("Default / ${Thread.currentThread().name}")
    }
    async(Dispatchers.IO) {
        println("IO / ${Thread.currentThread().name}")
    }
    async(Dispatchers.Unconfined) {
        println("Unconfined / ${Thread.currentThread().name}")
        delay(100L)
        println("Unconfined / ${Thread.currentThread().name}")
    }
    async(newSingleThreadContext("Geonhee Hwang")) {
        println("newSingleThreadContext / ${Thread.currentThread().name}")
    }
}
  • 예시코드(3)
  • 'Dispatchers.Unconfined'는 처음에는 부모의 Thread에서 수행 되지만 , 한 번 suspension point를 거치면 다른 Thread에서 수행한다. 이때 , 어느 Thread에서 수행 될 지 예측 할 수 없다. 따라서 가능하면 확실한 Dispatchers의 사용을 권장한다.
fun main() = runBlocking<Unit> {
    async(Dispatchers.Unconfined) {
        println("Unconfined / ${Thread.currentThread().name}")
        delay(1000L)
        println("Unconfined / ${Thread.currentThread().name}")
        delay(1000L)
        println("Unconfined / ${Thread.currentThread().name}")
        delay(1000L)
        println("Unconfined / ${Thread.currentThread().name}")
    }
}
  • 예시코드(4)
  • 'CoroutineScope' 와 'CoroutineContext'는 구조화되어 있고 부모에게 계층적으로 되어 있습니다. 따라서 CoroutineContext의 Job 역시 부모에게 의존적이여서 부모 Coroutine을 취소하면 자식 Coroutine도 취소된다.
  • 아래 코드에서는 부모가 있는 Job과 없는 Job에 관한 예시 코드이다.
  • 기본적으로는 Coroutine을 만들게 되면 , 부모의 Coroutine을 상속 받는다. 이는 Coroutine 들이 계층적 구조를 이루어서 상속 받는 다는 것을 알 수 있다.
  • 그러나 기본적인 Coroutine 이외에 launch(Job())을 이용해서 Coroutine을 만들게 되면 더 이상 계층적 구조가 아니게 된다.
  • 왜냐하면 기본적으로 만들어지는 job은 누가 부모인지 알지만, Job()은 누가 부모인지 모른다.
  • 구조적인 동시성이 있을때는 상위 코루틴이 하위 코루틴들이 모두 완료 될 때까지 기다리고 , 하위 코루틴 중 누군가 취소되면 상위 코루틴도 취소가 된다.
  • launch로 Job()을 활용해서 Coroutine을 만들면 생성된 Coroutine은 더 이상 구조적인 동시성을 갖는 코루틴이 아니게 된다.(Job()은 누가 부모 형제 코루틴인지 알 수 없기 때문에) 더 이상 구조적인 동시성을 갖는 코루틴이 아니라는 것은 의미상으로는 더 이상 상위 코루틴이 하위 코루틴을 기다려주지 않고 , 하위 코루틴이 취소 되어도 형제 코루틴과 상위 코루틴이 취소되지 않는다는거야 (사실은 독립적인 코루틴이라 상위 , 하위 , 형제 코루틴이 없지만)
fun main() = runBlocking {
    val job = launch {
        launch(Job()) {
            println(coroutineContext[Job])
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("3!")
        }

        launch {
            println(coroutineContext[Job])
            println("launch2: ${Thread.currentThread().name}")
            delay(1000L)
            println("1!")
        }
    }
    delay(500L)
    job.cancelAndJoin()
    delay(1000L)
}
  • 예시코드(5)
  • CoroutineElement 간의 결합 : '+' 연산자를 사용해서 할 수 있으며 이렇게 합쳐진 Coroutine들은 'coroutineContext[???]'를 통해서 조회 할 수 있다. 이로서 다양한 형태로 코루틴을 생성 할 수 있는 것이다.
  • 상위 코루틴의 컨텍스트와 하위 코루틴의 컨텍스트를 합친 것이 하위 코루틴의 컨텍스트이다.
@OptIn(ExperimentalStdlibApi::class)
fun main() = runBlocking {
    launch {
        launch(Dispatchers.IO + CoroutineName("launch1")) {
            println("launch1: ${Thread.currentThread().name}")
            println(coroutineContext[CoroutineDispatcher])
            println(coroutineContext[CoroutineName])
            delay(5000L)
        }
        launch(Dispatchers.Default + CoroutineName("launch2")) {
            println("launch2: ${Thread.currentThread().name}")
            println(coroutineContext[CoroutineDispatcher])
            println(coroutineContext[CoroutineName])
            delay(10L)
        }
    }
}

CEH && SupervisorJob && supervisorScope

CEH

-CEH(Coroutine Exception Handler)는 코루틴에서 발생 할 수 있는 예외를 체계적으로 관리 할 수 있게 해주는 것이다.

-CEH를 사용해서 자신만의 CHE를 만든 후 CEH를 사용해서 코루틴에서 발생하는 예외를 체계적으로 처리하고 싶은 Coroutine Builder 또는 CoroutineScope 의 Parameter에 넣어주면 된다. CEH도 CoroutineContext의 일부 이기 때문에 Coroutine Builder 또는 CoroutineScope의 Parameter에 넣어 줄 수 있는 것이다.

-단, CHE는 계층적인 구조를 가지는 Coroutine에서는 사용 할 수 없다.

-CoroutineExceptionHandler{ CoroutineContext , exception -> } 이런식으로 특정 변수에 넣어서 사용해주면된다. 일반적으로는 CoroutineContext는 _로 처리를 하는데 이는 우리가 궁금한건 어떤 CoroutineContext에서 에러가 발생하였는지는 중요하지 않고, 어떤 에러가 발생한지 중요하기 때문이다.

-try~catch~finally 문으로 에러처리를 다루는 것보다 관리하기가 편하다는 이점이 존재한다.

  • 예시코드(1)
suspend fun printRandom3_1() {
    delay(1000L)
    println(Random.nextInt(0 , 500))
}

suspend fun printRandom3_2() {
    delay(500L)
    throw ArithmeticException()
}

val ceh = CoroutineExceptionHandler { _ , exception -> // _자리는 coroutineContext 자리
    println("Something happend: $exception")
}

@OptIn(ExperimentalStdlibApi::class)
fun main() = runBlocking {
    val scope = CoroutineScope(Dispatchers.IO)
    val job = scope.launch(ceh + Dispatchers.Default) {
        launch { printRandom3_1() }
        launch { printRandom3_2() }
    }
    println(coroutineContext[CoroutineDispatcher])
    job.join()
}

-CEH는 runBlocking에서는 사용이 불가능하다. runBlocking은 자식이 예외로 종료되면 항상 종료되고 CEH를 호출하지 않기 때문이다.

  • 예시코드(2)
  • 이 아래의 코드에서는 CEH가 발생하면 에러가 타고 타고 올라가서 runBlockig이 종료되기 때문이다. 그래서 이를 해결하기 위한 아래의 SupervisorJob이 존재한다.
suspend fun getRandom1(): Int {
    delay(1000L)
    return Random.nextInt(0, 500)
}

suspend fun getRandom2(): Int {
    delay(500L)
    throw ArithmeticException()
}

val ceh = CoroutineExceptionHandler { _, exception ->
    println("Something happend: $exception")
}

fun main() = runBlocking<Unit> {
    val job = launch (ceh) {
        val a = async { getRandom1() }
        val b = async { getRandom2() }
        println(a.await())
        println(b.await())
    }
    job.join()
}

SupervisorJob

-일반적인 Job 객체는 예외가 발생되면 취소 요청을 위아래로 보내게 된다. 따라서 하위 코루틴에서 에러가 발생하면 위로 올라가고 내려가고 아주 난리가 난다. 따라서 최상위 코루틴이 취소가 되면 그 아래 자식들이 전부 다 취소가 되는 문제가 존재하였다.

-그래서 이러한 문제를 해결하기 위한 SupervisorJob이라는 것이 등장하였고, 이는 예외에 의한 취소가 전달되는 방향이 오직 아랫방향뿐이다.

-SupervisorJob 또한 결국에는 Job을 상속받았기 때문에 CoroutineContext에 더해 질 수 있어서, SupervisorJob을 사용하고 싶어하는 코루틴 빌더 또는 CoroutineScope의 Parameter에 넣어주면된다.

  • 예시코드(1)
  • SupervisorJob 때문에 printRandom5_2()에서 발생한 예외가 아래로만 내려가서 scope에 영향을 주지 않아서 job1도 영향을 받지 않게 되어서 job1 객체는 안전하게 실행이 가능해진다.
suspend fun printRandom5_1() {
    delay(1000L)
    println(Random.nextInt(0 , 500))
}

suspend fun printRandom5_2() {
    delay(500L)
    throw ArithmeticException()
}

val ceh5 = CoroutineExceptionHandler { _, exception ->
    println("Something happend: $exception")
}

fun main() = runBlocking {
    val scope = CoroutineScope(Dispatchers.IO + SupervisorJob() + ceh5)
    val job1 = scope.launch { printRandom5_1() }
    val job2 = scope.launch { printRandom5_2() }
    joinAll(job1 , job2)
}

supervisorScope

-supervisorScope는 suspendFunction이며, 이를 사용 할 때에 주의사항은 반드시 코드 블락 내부에서 에러가 발생할 코루틴의 Context에 CEH 처리를 해주거나 try~catch~finally 처리를 해주어야한다. 아무리 supervisorScope 이더라도 이러한 예외 처리를 해주지 않으면 에러가 위 아래로 퍼지게 된다.

  • 예시코드(1)
suspend fun printRandom6_1() {
    delay(1000L)
    println(Random.nextInt(0 , 500))
}

suspend fun printRandom6_2() {
    delay(500L)
    throw ArithmeticException()
}

suspend fun supervisoredFunc() = supervisorScope {
    // supervisorScope 코드 블락 내부에 에러가 발생할 곳에 반드시 ceh를 붙히거나 try-catch를 해야한다. 아니면 그냥 에러가 발생해버린다.
    launch { printRandom6_1() }
    launch(ceh6) { printRandom6_2() }
}

val ceh6 = CoroutineExceptionHandler { _, exception ->
    println("Something happend: $exception")
}

fun main() = runBlocking{
    val scope = CoroutineScope(Dispatchers.IO)
    val job = scope.launch {
        supervisoredFunc()
    }
    job.join()
}

SharedObject && Mutex && Actor

-여러 thread를 코루틴이 사용하기 때문에 가시성 , 동시성 문제가 발생한다. 객체의 값을 인식하는 것이 코루틴들마다 다를 수 있는 문제가 있기 때문에 SharedObject , Mutex , Actor 등을 활용하면 동시성 문제를 해결 할 수 있다.

  • 가시성 , 동시성 문제 예시코드(1)
  • 아래 코드를 실행하면 100,000이 나올 것같지만 실제로는 100,000이 출력되지 않고 실행 할 때마다 값이 달라지는데 이는 [가시성 문제] : 다른 스레드의 코루틴이 counter++를 하는 동안 올리기전의 값을 보고 현재 값인 줄 알고 counter++ 할 수도 있고 , [동시성 문제] : 코루틴이 counter++를 실행했는데 다른 스레드의 코루틴이 동시에 counter++를 하고 있을 수 도 있따.
suspend fun massiveRun(action: suspend() -> Unit) {
    val n = 100
    val k = 1000
    val elapsed = measureTimeMillis {
        coroutineScope {
            repeat(n) {
                launch {
                    repeat(k) { action() }
                }
            }
        }
    }
    println("$elapsed ms동안 ${n * k}개의 액션을 수행했습니다.")
}

var counter = 0

fun main() = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            counter ++
        }
    }
    // Counter = 100000 이라고 출력을 희망하지만 그렇지 않다.
    // 가시성이라는 것은 하나의 Coroutine에서 값을 수정했을 때 다른 쪽에서 값을 제대로 볼 수 있어야 되는 것이야.
    // 왜냐하면 counter에 값을 더할 때 기준값을 같은 값으로 보고 올릴 수 있기 때문이다.
    println("Counter = $counter")
}
  • 동시성 문제 예시코드(2)
  • 아래 코드에서는 @Volatile 어노테이션을 활용해서 가시성 문제는 해결했지만, 동시성 문제는 여전히 남아있다. @Volatile 어노테이션을 활용해서 선언한 변수의 값이 변경되면 어떤 thread에서 변경을 해도 다른 thread에게도 값이 영향을 준다.
  • @Volatile은 가시성 문제는 해결 할 수 있지만, 동시성 문제는 해결 할 수 없다.
suspend fun massiveRun2(action: suspend() -> Unit) {
    val n = 100
    val k = 1000
    val elapsed = measureTimeMillis {
        coroutineScope {
            repeat(n) {
                launch {
                    repeat(k) { action() }
                }
            }
        }
    }
    println("$elapsed ms동안 ${n * k}개의 액션을 수행했습니다.")
}
@Volatile // @Volatile 어노테이션을 사용해서 지정한 값은 어떤 스레드에서 변경을 해도 다른 Thread에게 값이 영향을 준다.
var counter2 = 0

fun main() = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun2 {
            counter2++
        }
    }
    // Counter = 100000 이라고 출력을 희망하지만 그렇지 않다.
    // 가시성이라는 것은 다른 스레드의 Coroutine에서 값을 수정했을 때 또 다른 스레드의 코루틴에서 값을 제대로 볼 수 있어야 되는 것이야.
    // @Volatile 어노테이션으로 인해서 100,000개의 코루틴이 가시성은 생겼으나 다수의 코루틴이 같은 값을 +1씩 증가 시키는 문제는 여전히 존재한다. (증가가 무시됨)
    println("Counter = ${counter2}")
}
  • 해결 예시코드(1)
  • Atomic 관련 자료형으로 선언 : 그러나 모든 문제에 적용되지는 않는다.
  • Atomic 관련 자료형은 원자성 연산은 가능하지만, 여러 연산을 조합하는 복잡한 연산의 경우 추가적인 동기화가 필요 할 수 있는 등의 여러 가지 복잡한 생각들을 해야하기 때문에 모든 문제에 적용되지는 않는다.
// 모든 문제에 적용되지 않는다.
suspend fun massiveRun3(action: suspend () -> Unit) {
    val n = 100
    val k = 1000
    val elapsed = measureTimeMillis {
        coroutineScope {
            repeat(n) {
                launch {
                    repeat(k) { action() }
                }
            }
        }
    }
    println("$elapsed ms동안 ${n * k}개의 액션을 수행했습니다.")
}
val counter3 = AtomicInteger()

fun main() = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun3 {
            counter3.incrementAndGet()
        }
    }
    println("Counter = $counter3")
}
  • 해결 예시코드(2)
  • Thread를 단일 스레드로 한정 시킨 후 문제 해결하는 방법: 모든 코드에서 단일 스레드에서 사용 할 수 있고, 일부의 스레드에서 사용 할 수 도 있다. ( 상황에 따라 다르다. )
  • 즉, 전체코드를 하나의 스레드에서 동작시킬 것이지 , 아니면 전체코드 중 특정코드만 하나의 스레드에서 동작시킬 것인지에 대한 문제이다.
suspend fun massiveRun4(action: suspend () -> Unit) {
    val n = 100
    val k = 1000
    val elapsed = measureTimeMillis {
        coroutineScope {
            repeat(n) {
                launch {
                    repeat(k) { action() }
                }
            }
        }
    }
    println("$elapsed ms 동안 ${n * k}개의 액션을 수행했습니다.")
}
var counter4 = 0
val counterContext = newSingleThreadContext("CounterContext")

fun main() = runBlocking {
    withContext(counterContext) {
        massiveRun4 { counter4 ++ }
    }
    println("Counter = ${counter4}")
}

Mutex

-Mutex(MutualExclusion) : 상호배제의 줄임말이다. 이를 활용하면 공유 상태를 수정할 때 임계 영역(critical section)을 이용하게 하며 , 임계 영역은 스레드가 동시에 접근하는 것을 허용하지 않는다.

-val mutex = Mutex() 이렇게 임계영역을 만든 후 mutex.withLock {}을 활용해서 접근을 허용하는 코드를 작성한 후 코드 내부에서 필요한 로직을 작성하면 된다.

  • 예시코드(1)
suspend fun massiveRun(action: suspend () -> Unit) {
    val n = 100
    val k = 1000
    val elapsed = measureTimeMillis {
        coroutineScope {
            repeat(n) {
                launch {
                    repeat(k) { action() }
                }
            }
        }
    }
    println("$elapsed ms 동안 ${n * k}개의 액션을 수행했습니다.")
}

val mutex = Mutex()
var counter = 0

fun main() = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            mutex.withLock { 
 > critical section을 만드는 부분으로 , 여러 스레드 중 하나의 스레드 만 'counter++'를 실행 할 수 있다.
                counter++
            }
        }
    }
    println("Counter = $counter")
}

Actor

-Actor는 독점적으로 자료를 가지고 그 자료를 다른 코루틴과 공유하지 않고 그 자료를 이용하기 위해서는 무조건 Actor를 통해서만 접근할 수 있다.

-자료를 관리하는 Actor를 만들고 , 그 액터에게 신호를 보내서 우리가 원하는 명령을 시키고 결과를 얻는 형태이다.

-Actor를 사용하는 과정[1.sealed class 생성][2.실제 Actor 생성]

-Actor를 만드는 방법은 1.actor 함수호출 2.블록 내에 우리가 원하는 로직을 설정(상태값을 캡슐화) , 외부에서 Actor로 보내는 것은 채널을 통해서만 받을 수 있어.

  • 예시코드(1)
suspend fun massiveRun(action: suspend () -> Unit) {
    val n = 100
    val k = 1000
    val elapsed = measureTimeMillis {
        coroutineScope {
            repeat(n) {
                launch {
                    repeat(k) { action() }
                }
            }
        }
    }
    println("$elapsed ms 동안 ${n * k}개의 액션을 수행했습니다.")
}
// Actor : Actor가 독점적으로 자료를 가지게 되고 그 자료를 다른 코루틴과 공유하지 않고 그 자료를 이용하기 위해서는 무조건 Actor를 통해서만 접근할 수 있다.
// 자료를 관리하는 Actor를 만들고 , 그 액터에게 신호를 보내서 우리가 원하는 결과를 얻는 형태
// Actor를 사용하는 과정[1.sealed class 생성]
sealed class CounterMsg {
    object IncCounter : CounterMsg() // Actor에게 값을 증가시키는 신호 보내
    class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() // Actor에게 값을 가져오라는 신호 보내
}
fun CoroutineScope.countActor() = actor<CounterMsg> {// actor 내부에 상태를 캡슐화 시켜서 다른  코루틴이 접근하지 못하게 한다.
    var counter = 0
    // suspension point : 값이 오기까지 기다렸다가 값이 오면 깨어난다. (아래 코드)
    for (msg in channel) { // 외부에서 보내는 것은 채널을 통해서만 받을 수 있다.(recieve) : 한쪽에서는 데이터를 보내고 다른쪽에서는 데이터를 받을 수 있는 것.
        // channel은 송신 측에서 갑을 보낼 수 있고 , 수신 측에서 값을 받을 수 있는 도구이다.
        when (msg) {
            is CounterMsg.IncCounter ->counter ++
            is CounterMsg.GetCounter -> msg.response.complete(counter)
        }
    }
}
// ** 주는 쪽도 받는 쪽이 끝날때까지 기다렸다가 깨어나고 , 받는 쪽도 주는 쪽이 없으면 잠이 들었다가 깨어난다. ** //
fun main() = runBlocking<Unit> {
    val counter = countActor()
    withContext(Dispatchers.Default) {
        massiveRun {
            counter.send(CounterMsg.IncCounter) // countActor()를 사용하기 위해서는 오로지 채널을 통해서 시그널을 보내야 한다. : actor가 직접 값을 더하게 한다.
        }
    }
    val response = CompletableDeferred<Int>()
    counter.send(CounterMsg.GetCounter(response)) // countActor()를 사용하기 위해서는 오로지 채널을 통해서 시그널을 보내야 한다. : actor가 값을 가져온다.
    println("Counter = ${response.await()}") // suspension point
    counter.close() // 0
}
profile
포기하지 말기

0개의 댓글