[코루틴 #2  동작 제어하기] - Dispatcher(문맥 지정), 작업 반복/ 취소/ 실행 보장/ 시간 만료
Language/코루틴

[코루틴 #2 동작 제어하기] - Dispatcher(문맥 지정), 작업 반복/ 취소/ 실행 보장/ 시간 만료

728x90
반응형

Context(문맥)


코루틴은 항상 Context(컨텍스트-문맥) 안에서 실행됩니다.

Context(문맥)는 코루틴이 어떻게 실행되고 동작해야 하는지를 정의할 수 있게 해주는 요소들의 그룹입니다.

Context는 또한 결합이 될 수 있고, 분리하여 제거할 수도 있습니다.

 

코루틴의 scope코루틴이 실행되는 Context를 정의합니다.
scope는 코루틴의 jop과 dispatchers에 대한 정보를 결합합니다.
scope는 코루틴을 추적하는데, 코루틴을 시작하면 "scope에 포함"됩니다.

이는 코루틴을 추적할 scope를 지정했음을 의미합니다.

 

코루틴이 실행될 때 어러 가지 Context는 CoroutineContext에 의해 정의됩니다. 

launch{ ... }와 같이 인자가 없는 경우에는 CorountineScope에서 상위 문맥이 상속되어 결정되고 lauch(Dispatchers.Default) {...}와 같이 사용되면 GlobalScope에서 실행되는 문맥과 동일하게 사용됩니다. 

GlobalScope는 메인 스레드의 생명주기가 끝나면 같이 종료됩니다. 

CoroutineContext 더 알아보기

 

 

내부적으로 보통 CommonPool이 지정되어 코루틴이 사용할 스레드의 공동 풀(pool)을 사용하게 됩니다.

이것은 이미 초기화되어 있는 스레드 중 하나 혹은 그 이상이 선택되며 초기화하기 때문에 스레드를 생성하는 오버헤드가 없어 빠른 기법입니다. 그리고 하나의 스레드에 다수의 코루틴이 동작할 수 있습니다.

만일 특정 스레드 개수를 직접 지정하려면 다음과 같이 사용자 문맥을 만들어 지정할 수 있습니다 .

 

val threadPool = Excutors.newFixedThreadPool(4)
val MyCintext = threadPool.asCoroutineDispatcher()
...
async(MyContext{...}
...

 

cf) withContext

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

이미 일시 중단 함수 상태에 있을 때 withContext()를 사용해 코드 블록에 대한 컨텍스트를 변경할 수 있다. withContext()는 async와 동일한 역할을 하는 키워드인데, 차이점은 await()를 호출할 필요가 없으면 마지막 구문에 해당하는 결과가 리턴될 때까지 기다린다. 즉, 프로세스에 Job을 포함시키지 않고도 다른 컨텍스트로 전환할 수 있게 해주는 일시 중단 함수이다.


Dispatcher



실행 문맥 지정하기

 

코루틴은 항상 특정 문맥에서 실행되는데, 이때 어떤 문맥에서 코루틴을 실행할지 이 Dispatcher(디스패처)가 결정합니다. Dispatcher는 다양한 스레드에서 실행되도록 코루틴을 보냅니다.

예를 들어 Dispatchers.Main은 기본 스레드에서 작업을 실행하고 Dispatchers.IO는 차단 I / O 작업을 스레드의 공유 풀로 보냅니다.

fun main() = runBlocking<Unit> {

	val jobs = arrayListOf<Job>() 
    
    jobs += launch(Dispatchers.Unconfined) { // main 스레드에서 작업 --- (3) 
    	println("Unconfined:\t\t ${Thread.currentThread().name}") 
    } 
    
    jobs += launch(coroutineContext) { // 부모의 문맥, 여기서는 runBlocking의 문맥 
    	println("coroutineContext:\t ${Thread.currentThread().name}") 
    } 
    
    jobs += launch(Dispatchers.Default) { // 디스패처의 기본값 --- (1) 
    	println("Default:\t\t ${Thread.currentThread().name}") 
    } 
    
    jobs += launch(Dispatchers.IO) { // 입출력 중심의 문맥 --- (2) 
    	println("IO:\t\t ${Thread.currentThread().name}") 
    } 
    
    jobs += launch { // 아무런 인자가 없을 때 
    	println("main runBlocking: ${Thread.currentThread().name}") 
    } 
    
    jobs += launch(newSingleThreadContext("MyThread")) { // 새 스레드를 생성함 --- (4) 
    	println("MyThread:\t\t ${Thread.currentThread().name}") 
    } 
    
    jobs.forEach { 
    	it.join() 
    } 
    
}

코루틴은 CoroutineContext으로 구현된 형식의 문맥을 가집니다.
CoroutineDispatcher는 추상 클래스로 몇 가지 디스패처 객체를 정의하고 있습니다.


(1) 기본 문맥
Dispatchers.Default는 기본 문맥인 CommonPool에서 실행되고 GlobalScope로도 표현됩니다.
따라서 launch(Dispatcher.Default) {...}와 GlobalScope.lauch{...}는 같은 표현입니다.
이것은 공유된 백그라운드 스레드의 CommonPool에서 코루틴을 실행하도록 합니다.
다시 말하면 스레드를 새로 생성하지 않고 기존에 있는 것을 이용합니다.
따라서 연산 중심의 코드에 적합합니다.

(2) I/O를 위한 문맥
Dispatchers.I/O는 입출력 위주의 동작을 하는 코드에 적합한 공유된 폴더입니다.
따라서 블로킹 동작이 많은 파일이나 소켓 I/O 처리에 사용하면 좋습니다.


(3) Unconfined 문맥
Dispatchers.Unconfined는 호출자 스레드에서 코루틴을 시작하지만 첫 번째 지연점까지만 실행합니다.
특정 스레드나 풀에 가두지 않고 첫 번째 일시 중단 후 호출된 지연 함수에 의해 재개됩니다.
이 옵션을 사용하는 것은 권장하지 않습니다.


(4) 새 스레드를 생성하는 문맥
newSingleThreadContext는 사용자가 직접 새 스레드 풀을 만들 수 있습니다.
새 스레드를 만들기 때문에 비용이 많이 들고 더 이상 필요하지 않으면 해제하거나 종료시켜야 합니다.
이 옵션은 성능상의 이유로 향후 변경될 가능성이 습니다.

 

 

 

 


기본 동작 제어하기


1) repeat() 함수로 코루틴 반복적으로 동작하기

지속적으로 반복하는 코드를 작성하기 위해 repeat( )  함수를 이용할 수 있습니다. 그러면 백그라운드에서 실행하는 일종의 데몬(demone) 스레드를 구성할 수 있습니다.

 

fun main

1000회를 반복하기 위해 repeat( ) 함수에 1000이라는 인자를 주고 있습니다. 하지만 GlobalScope로 생명주기를 한정했기 때문에 메인 스레드가 종료되어 버리면 더 이상 진행되지 않습니다. 여기서 1.3초 뒤 종료되므로 약 3번 정도만 진행되고 중단됩니다. 만일 GlobalScope를 제거하면 모든 횟수를 진행할 때까지 프로그램이 종료되지 않습니다.

 


2) cancel() 함수로 코루틴 작업 취소하기

만일 join 함수만 사용하면, main( ) 함수가 job의 완료를 기다리기 때문에 repeat( ) 함수의 1000번의 반복 실행이 모두 진행됩니다. 

 

 


3) finally 코루틴 종료 과정 처리하기

try ~finally 구문을 사용해 finally 블록에서 코루틴의 종료 과정을 처리하도록 할 수 있습니다.

fun main( )  = runBlocking<Unit>{

}

실행결과

 

 

일반적인 finally 블록에서 지연 함수를 사용하려고 하면 코루틴이 취소되므로 지연 함수를 사용할 수 없습니다. 그 외에 파일을 닫ㄱ더나 통신 체널을 닫는 등의 작업은 넌블로킹 형태로 작동하며 지연함수를 포함하고 있지 않기 때문에 문제가 없습니다.

만일 finally 블록에서 시간이 걸리는 작업이나 지연 함수가 사용될 경우 실행을 보장하기 위해서는 nonCancellable 문맥에서 작동하도록 해야 합니다.

이것을 위해 withContext(NonCancellable { ... } 을 사용해 다음과 같이 finally  블록을 구성할 수 있습니다. 

다음은 코드는 1초 이후에도 println( ) 함수의 실행을 보장하는 예입니다.

...
finally{
	withContext(NonCancellable) { //finally의 완전한 실행을 보장함
    	println("I'm running finally")
        delay(1000L)
        println("Non-Cancellable") // 1초를 지연해도 취소되지 않음
	}
}

 

4) 코루틴 실행 상태 판단하기

만일 코드를 중단하기 위해 코루틴에 조건식을 넣으려고 할 때 연산이 마무리되기 전까지는 조건식에 의해 루틴이 중단되지 않는다는 것을 기억해야 합니다. 다음 코드를 봅시다.

fun main

delay(1300L) 이후 작업 취소 함수에 의해 시그널을 받아 루틴이 바로 취소될 것 같지만  while (i <5) {...} 루프를 사용하면 루프가 완료될 때까지 루틴이 끝나지 않습니다.

 

실행결과

 

취소 시그널을 받아 루프를 중단하려면 소스 코드에서 while (i <5)를 while(isActive)로 변경합니다. 다시 실행하면 의도한 시간에 루프가 취소되어 중단됩니다.

 

실행결과

 

 

5) 코루틴 만료 시간 설정하기

이번엔 일정 식행 시간 뒤에 코루틴을 취소할 수 있도록 해봅시다.

withTimeout( ) 지연 함수의 선언부는 다음과 같습니다.

 

 

다음은 시간이 만료되면 block을 취소시키고 timeoutCancellationException오류가 발생하는 코드입니다. 예외를 발생하지 않고 null 로 처리하려면 withTimeoutOrNull()을 사용합니다.

 

fun main( )  = 

실행 결과

728x90
반응형