[코틀린 동시성 프로그래밍] 4장 일시 중단 함수와 코루틴 컨텍스트 -2

Sdoubleu·2023년 1월 17일
0

코틀린 동시성

목록 보기
7/10
post-thumbnail

코루틴 컨텍스트

  • 코루틴은 항상 컨텍스트 안에서 실행됨

  • 컨텍스트
    코루틴이 어떻게 실행되고 동작해야 하는지를 정의할 수 있게 해주는 요소들의 그룹


디스패처

  • 디스패처
    코루틴이 실행될 스레드를 결정하는데,
    여기에는 시작될 곳과 중단 후 재개될 곳을 모두 포함

CommonPool

  • CommonPool
    CPU 바운드 작업을 위해서 프레임워크에 의해 자동으로 생성되는 스레드 풀

  • 스레드 풀의 최대 크기는 시스템의 코어 수에서 1을 뺀 값
    -> 현재는 기본 디스패처로 사용되지만 용도를 명시하고 싶다면,
    다른 디스패처처럼 사용할 수 있음

GlobalScope.launch(CommonPoo) {
	//TODO: Implement CPU-bound algorithm here
}

CommonPool이 기본 디스패처로써 지원하는 것을 중단할 것을 확실하지 않지만, 예상치 못한 변경에 대비하기 위해 CPU 바운드 작업에 명시적으로 사용하는 것을 고려

기본 디스패처

  • 현재는 CommonPool과 같음
    -> 기본 디스패처 사용을 위해서 디스패처 전달 없이 빌더를 사용할 수 있음
    CommonPool이 기본 디스패처로 지원되지 않는다.
    ComoonPool을 직접 사용하지 못하기 때문에 디스패처 없이 사용하거나, Dispatchers.Default를 사용하도록 수정했다.
GlobalScope.launch {
	//TODO:일시 중단 람다 구현
}

↪ 명시적으로 지정할 수 있음

  • DefaultDespatcher는 Dispatchers.Default로 변경됐다.
GlobalScope.launch(Dispatchers.Default) {
	//TODO:일시 중단 람다 구현
}

Unconfined

  • 첫 번째 중단 지점에 도달할 때까지 현재 스레드에 있는 코루틴을 실행

  • 코루틴은 일시 중지된 후에, 일시 중단 연산에서 사용된 기존 스레드에서 다시 시작

unconfined 함수 suspend a()가 특정 스레드에 있는 디스패처와 함께 실행되는 suspend b()를 호출할 경우, a()는 b()가 실행된 같은 스레드에서 다시 시작될 것이다.
이것은 일시 중단 연산을 컴파일하는 방식 때문에 발생한다.

ex)
fun main(args: Array<String>) = runBlocking {
	GlobalScope.launch(Dispatchers.Unconfined) {
    	println("Starting in ${Thread.currentThread().name}")
        delay(500)
        println("Resuming in ${Thread.currentThread().name}")
	}.join()
}

단일 스레드 컨텍스트

  • 항상 코루틴이 특정 스레드 안에서 실행된다는 것을 보장

  • 이 유형의 디스패처를 생성하려면 newSingleThreadContext() 를 사용해야 함

fun main(args: Array<String>) = runBlocking {
	val dispatcher = newSingleThreadContext("myThread")

	GlobalScope.launch(dispatcher) {
    	println("Starting in ${Thread.currentThread().name}")
        delay(500)
        println("Resuming in ${Thread.currentThread().name}")
    }.join()
}

스레드 풀

  • 스레드 풀을 갖고 있으며 풀에서 가용한 스레드에서 코루틴을 시작하고 재개함
    -> 런타임이 가용한 스레드를 정하고 부하 분산을 위한 방법도 정하기 때문에, 따로할 작업❌
fun main(args: Array<String>)  {
    runBlocking {
        val dispatcher = newFixedThreadPoolContext(4, "myPool")
        GlobalScope.launch(dispatcher) {
            println("Starting in ${Thread.currentThread().name}")
            delay(500)
            println("Resuming in ${Thread.currentThread().name}")
        }.join()
    }
}

예외 처리

  • 코루틴 컨텍스트의 또 다른 중요한 용도는
    예측이 어려운 예외에 대한 동작 을 정의하는 것
    -> 이러한 유형의 컨텍스트는 CoroutineExceptionHandler 를 구현해 만들 수 있음
fun main(args: Array<String>)  {
    runBlocking {
        val handler = CoroutineExceptionHandler({ context, throwable ->
            println("Error captured in  $context")
            println("Message: ${throwable.message}")
        })

        GlobalScope.launch(handler) {
            TODO("Not implemented yet!")
        }
        delay(500)
    }
}

↪ 예측이 어려운 예외에 대한 정보를 출력하는
CoroutineExceptionHandler 를 생성
그 다음 예외를 던지고, 앱에 메세지를 출력하기 위해 약간의 시간을 주는 코루틴을 시작

그러면 앱이 예외를 정상적으로 처리👍


Non-Cancellable

  • 코루틴의 실행이 취소되면 코루틴 내부에 CancellationException 유형의 예외가 발생하고 코루틴이 종료됨

  • 코루틴 내부에서 예외가 발생하기 때문에 try-finally 블록을 사용해 리소스를 닫는 클리닝 작업을 수행하거나 로깅을 수행할 수 있음

↪ 예상대로 1.2초 지연한 후 작업이 취소됐고, finally 블록에서 메세지를 출력

  • 실제로 코루틴이 종료되기 전에 5초 동안 멈추도록 finally 블록을 수정해보자

↪ 실행하면 그런 식으로 작동 ❌❌

  • finally 블록에서의 실제 지연은 일어나지 않았음

  • 코루틴은 일시 중단된 후 바로 종료
    -> 취소 중인 코루틴은 일시 중단될 수 없도록 설계됐기 때문⚡

  • 코루틴이 취소되는 동안 일시 중지가 필요한 경우
    NonCancellable 컨텍스트를 사용해야 함⚡


컨텍스트에 대한 추가 정보

  • 컨텍스트는 또한 결합된 동작을 정의해 작동하기도 함

  • 컨텍스트가 어떻게 보다 더 창의적인 방법으로 사용될 수 있는지 ?


컨텍스트 결합

컨텍스트의 일부분이 될 수 있는 여러 종류의 요소가 있음
다양한 요구사항을 만족하는 컨텍스트를 생성하기 위해 이러한 요소들을
결합시킬 수 있음

컨텍스트 조합

  • 특정 스레드에서 실행하는 코루틴을 실행하고 동시에 해당 스레드를 위한 예외처리를 설정한다고 가정

  • 이를 위해 더하기 연산자를 사용해 둘을 결합시킬 수 있음

fun main(args: Array<String>)  {
    runBlocking {
        val dispatcher = newSingleThreadContext("myDispatcher")
        val handler = CoroutineExceptionHandler({ _ , throwable ->
            println("Erroe Captured")
            println("Message: ${throwable.message}")
        })
        GlobalScope.launch(dispatcher + handler) {
            println("Running in ${Thread.currentThread().name}")
            TODO("Not Implemented!")
        }.join()
    }
}

조합된 컨텍스트를 유지할 변수를 만들면, 한 번 이상 더하기 연산자를 사용하지 않아도 된다.
val context = dispatcher + handler
launch(context)

컨텍스트 분리

  • 결합된 컨텍스트에서 컨텍스트 요소를 제거할 수도 있음

  • 이렇게 하려면 제거할 요소의 키에 대한 참조 가 있어야 함

↪ launch(handler) { ... } 를 사용할 때와 같음

↪ 여기서 스레드는 dispatcher의 스레드가 아닌 기본 디스패처에 해당


withContext를 사용하는 임시 컨텍스트 스위치

  • 이미 일시 중단 함수 상태에 있을 때 withContext() 를 사용해 코드 블록에 대한 컨텍스트를 변경할 수 있음
    -> withContext()
    코드 블록 실행을 위해 주어진 컨텍스트를 사용할 일시 중단 함수

  • 다른 스레드에서 작업을 실행해야 할 필요가 있다면 계속 진행하기 전에 해당 작업이 끝날 때까지 항상 기다리게 됨

↪ 여기서는 컨텍스틑 디스패처를 이용한 작업을 위해 async() 를 사용하는데,
async()Deferred<String>을 반환하기 때문에
name이 준비될 때까지 일시 중단할 수 있도록 바로 await() 를 호출

  • withContext() 를 사용할 수 있음
    withContext() 함수는 Job이나 Deferred을 반환하지 않음❌❌


⭐정리

  • RecyclerView를 만들었고, RSS 피드에서 뉴스를 보여주는 약간의 안드로이드 프로그래밍으로 시작했다.

  • 어댑터를 통해 뷰에 데이터를 집합을 매핑하는 방법과 ViewHolder가 뷰의 일부로 사용되는 방법에 대해 이야기했다.

  • 안드로이드의 RecyclerView가 많은 뷰를 생성하지 않는다는 것을 배웠다.
    대신 사용자가 스크롤할 때 재활용된다.

  • 일시 중단 함수에 대해 이야기했으며
    withContext() 일시 중단 코드 정의를 위한 유연한 방법을 제공한다는 것을 배웠다.

  • 비동기 함수(Job 구현을 반환하는 함수)는 특정 구현 강요하는 위험을 피하기 위해 withContext() 공개 API의 일부가 돼서는 안된다고 언급했다.

  • 디스패처를 시작으로 예외 처리와 취소 불가능한
    고유한 컨텍스트(unique non-cancellable context)로 옮겨가는
    다양한 유형의 코루틴 컨텍스트를 나열했다.

  • 코루틴에서 기대하는 동작을 얻기 위해 많은 컨텍스트를 하나로 결합하기 위한 방법을 배웠다.

  • 요소 중 하나의 키를 제거함으로써 결합된 컨텍스트를 분리하는 세부적인 부분을 익혔다.

  • withContext()라는 흥미로운 함수를 알아봤는데,
    프로세스에 잡을 포함시키지 않고도 다른 컨텍스트로 전환할 수 있게 해주는 일시 중단 함수다.

profile
개발자희망자

0개의 댓글