코루틴과 동시성 프로그래밍 (3/3)

장똑대·2022년 5월 6일

Do it! 코틀린 프로그래밍 [셋째마당, 코틀린 표준 라이브러리의 활용] 학습

✏️3.코루틴 동작 제어하기✏️

3-1. 코루틴의 문맥

  • 코루틴은 항상 특정 문맥에서 실행
  • 어떤 문맥에서 코루틴을 실행할지는 디스패처(Dispatcher)가 결정
fun main() = runBlocking<Unit> {
    val jobs = arrayListOf<Job>()
    // (4) Unconfined 문맥
    jobs += launch(Dispatchers.Unconfined) {
        println(Thread.currentThread().name)
    }

    jobs += launch(coroutineContext) {
        println(Thread.currentThread().name)
    }

    // (1) 디스패처의 기본값
    jobs += launch(Dispatchers.Default) {
        println(Thread.currentThread().name)
    }
	
    // (2) 입출력 중심의 문맥
    jobs += launch(Dispatchers.IO) {
        println(Thread.currentThread().name)
    }
	
    jobs += launch {
        println(Thread.currentThread().name)
    }
	// (3) 새 스레드를 생성
    jobs += launch(newSingleThreadContext("MyThread")) {
        println(Thread.currentThread().name)
    }

    jobs.forEach { it.join() }
}
  • (1) 기본 문맥 (Default)
    • 공유된 백그라운드 스레드의 CommonPool에서 코루틴을 실행
    • GlobalScope로도 표현, GlobalScope.launch{}와 같은 표현
    • 스레드를 새로 생성하지 않고 기존에 있는 것을 이용
    • 연산 중심의 코드에 적합

  • (2) I/O를 위한 문맥
    • 입출력 위주의동작을 하는 코드에 적합한 공유된 풀
    • 블로킹이 많은 파일이나 소켓 I/O처리에 적합

  • (3) 새 스레드를 생성하는 문맥
    • 사용자가 직접 새 스레드 풀을 생성
    • 비용이 많이 들고 더 이상 필요하지 않으면 해제하거나 종료시켜야 함
    • 코루틴 안에 또 다른 코루틴을 정의하면 자식 코루틴이 됨
    • 부모가 취소(cancle)되는 경우 자식 코루틴은 재귀적으로 취소

  • (4) Unconfined문맥
    • 호출자 스레드에서 코루틴을 시작하지만 첫 번째 지연점까지만 실행
    • 특정 스레드나 풀에 가두지 않고, 첫 번쨰 일시 중단 후 호출된 지연 함수에 의해 재개
    • 이 옵션은 권장하지 않음

3-2. 기본 동작 제어하기

📌 repeat() 함수를 사용한 반복 동작하기

  • 지속적으로 반복하는 코드를 작성하기 위해 repeat()함수 이용
  • 백그라운드에서 실행하는 일종의 데몬(daemon)스레드를 구성
GlobalScope.launch {
	repeat(1000) { i ->
    	delay(500L)
    }
}

-> repeat()함수에 인자를 주어 1000회 반복
-> GlobalScpoe로 생명주기를 한정했기 때문에 메인 스레드가 종료되면 더 이상 진행되지 않음
-> 만약 GlobalScope를 제거하면 모든 횟수를 진행할 때까지 프로그램이 종료되지 않음

📌 코루틴 작업 취소하기

  • 만일 join()함수만 사용하면, main()함수가 job의 완료를 기다리게됨
  • cancel()함수를 사용하면 job은 작업을 취소하고 main()을 종료시킴
val job = launch {
	repeat(1000) {
    }
}
delay(1300L)
job.cancel()

-> job을 1000번 반복하기 전에 1.3초 후 main()함수는 종료됨

📌 finally의 실행 보장

  • finally 블록에서 지연 함수를 사용하려고 하면 코루틴이 취소되므로 지연 함수를 사용할 수 없음
  • 만일 finally 블록에 시간이 걸리는 작업이나 지연 함수가 사용될 경우 실행을 보장하기 위해서는 NonCancellable 문맥에서 작동하도록 해야함
...
} finally {
	withContext(NonCancellable) { // finally의 완전한 실행을 보장
    	delay(1000L)
        println("Non-Cancellable") // 1초를 지연해도 취소되지 않음
	}
}

📌 코루틴의 시간 만료

  • withTimeout() 함수 사용
fun main() = runBlocking<Unit> {
	try {
    	withTimeout(1300L) { // Timeout 예외 발생
        	repeat(1000) {
            	delay(500L)
            }
        }
    } catch (e: TimeoutCancellationException){
    }
}

-> 예외가 아닌 null로 처리하려면 withTimeoutOrNull()을 사용


3-3. 채널(Channel)의 동작

  • 채널은 자료를 서로 주고받기 위해 약속된 일종의 통로 역할
  • 코루틴의 채널은 넌블로킹 전송 개념으로 사용
  • 채널을 구현할 때는 SendChannel과 ReceiveChannel 인터페이스를 이용해 값들의 스트림을 전송하는 방법을 제공
fun main() = runBlocking<Unit> {
	val channel - Channel<Int>()
    launch {
    	for (x in 1..5) channel.send(x * y)
    }
    repeat(5) { println(channel.receive) }
}

-> 채널을 통해 send() 함수로 값을 보내 놓으면 이후 receive() 함수를 통해 값을 받을 수 있음
-> 일반 큐와는 다르게 더이상 전달 요소가 없으면 채널을 닫을 수 있음
-> for문을 구성해 채널을 받고 close()를 사용하면 바로 채널을 닫는 것이 아니라 닫겠다는 특수한 토큰을 보냄


3-4. produce 생산자 소비자 패턴

  • produce는 채널이 붙어 있는 코루틴으로 생산자 측면의 코드를 쉽게 구성할 수 있음
  • 소비자는 consumeEach함수를 확장해 for문을 대신해서 저장된 요소를 소비
// 생산자를 위한 함수 생성
fun CoroutineScope.producer(): ReceiveChannel<Int> = produce {
	val total: Int = 0
    for (x in 1..5) {
    	total += x
        send(total)
    }
}

fun main() = runBlocking {
	val result = producer() // 값의 생산
    result.consumeEach { print($it) } // 소비자 루틴 구성
}

📌 버퍼를 가진 채널

  • 채널에는 기본 퍼버가 없으므로 send() 함수가 먼저 호출되면 receive() 함수가 호출되기 전까지 send() 함수는 일시 지연
  • 채널에 버퍼 크기를 주면 지연 없이 여러 개의 요소를 보낼 수 있음
  • Channel() 생성자의 capacity 매개변수를 통해 버퍼 크기를 정할 수 있음
fun main() = runBlocking {
    val channel = Channel<Int>(2) // 버퍼 capacity 값을 줌
    val sender = launch(coroutineContext) { // 송신자 측
        repeat(10) {
            channel.send(it) // 지속적으로 보내다가 꽉 차면 일시 지연
        }
    }
    delay(1000) // 아무것도 받지 않고 1초 기다린 후
    sender.cancel() // 송신자의 작업을 취소
}
profile
장똑대와 안드로이드

0개의 댓글