#코루틴 학습 링크 1 : https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md
#코루틴 학습 링크 2 : https://kotlinlang.org/docs/coroutines-guide.html#additional-references
-스쿱 내부의 코드들은 코루틴 코드 / 스쿱 내부의 코드들은 코루틴 코드(x)
-코루틴 코드의 일부를 함수로 분리하고 싶을 때에는 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을 사용해서 대기상태에 넣어둔 후 에 스레드에서 대기중인 코루틴을 선택하면서 서로 번갈아 가면서 실행 시키는 것이다.
-하지만, 코루틴이 다른 스레드로 분산된다면 각각의 스레드에서 코루틴을 실행시키기 때문에 동시에 실행 할 수 있기도 한다.
// 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()
}
-parent가 어떤 이유로든 취소되면, parent의 모든 children이 취소된다.
-child에서 exception이 던져져서 취소되면, exception은 parent로 전파되어서 parent를 취소시킨다. child가 명시적인 취소로 인해 취소되면 parent로 취소가 전파되지 않는다.
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)
}
-runBlocking은 runBlocking을 호출한 Thread를 차단한 채로 코루틴 스코프를 생성한다. 해당 Thread는 코루틴의 진행이 완료 될때까지 차단된다. 즉, runBlocking이 생성한 CoroutineScope 내부의 코드 실행을 보장한다. 따라서 MainThread에서 runBlocking을 사용할 때 주의해야 한다.
-runBlocking{}은 suspend function을 포함하는 순차적인 코드를 작성하고자 할 때 또는 테스트 할 경우에 주로 사용한다.
-만약 MainThread에서 runBlocking을 사용했을 때는 ANR 에러가 발생할 수 도 있으므로 주의해야 한다.
fun main() = runBlocking {
// 'Thread.currentThread().name'을 활용해서 현재 사용되고 있는 코루틴의 이름을 알 수 있다.
println(Thread.currentThread().name)
println("Hello")
}
// runBlocking은 자신의 코드 블록이 종료되기 전까지 다른 Coroutine이 실행되는 것을 막는다.
fun main() = runBlocking {
println(coroutineContext)
println(Thread.currentThread().name)
println("Hello")
}
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{}는 runBlocking과는 다르게 단독으로는 CoroutineScope를 구성 할 수 없으며, 오직 코루틴 스코프 내부에서만 사용이 가능하다.
-launch{}는 가능하다면 다른 코루틴과 병렬적으로 실행이 가능하며 , launch{}의 코드블록은 바로 실행되는 것이 아니라 실행될 준비가 완료되면 실행된다.
-launch(start = CoroutineStart.LAZY)를 사용하면 시작을 늦출 수 있다. 사용방법은 async(start = CoroutineStart.LAZY)와 동일하다.
-Job 객체를 반환한다. Job에 대한 설명은 아래 스크롤 내리다 보면 있다.
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{} 블록은 새로운 코루틴을 생성하여 비동기적으로 실행을
시작한다.
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")
}
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 이렇게 출력된다.
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{}가 종료되기 전까지 종료되지 않습니다.
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는 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가 코루틴을 생성할 준비를 시작하고 , 코루틴을 생성한다.
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)
}
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() 메서드를 사용하면 된다.
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")
}
}
-현재는 DelicateCoroutinesApi 상태로 삭제된 API라는 뜻이다.
-어떤 계층에도 속하지 않고 영원히 동작하게 된다는 문제점이 있어서 삭제되었고 현재는 Coroutine Scope만 남아있다.
-CoroutineScope는 parameter로 CoroutineContext를 받고(Coroutine Element 하나만 넣어도 된다.), 우리는 이 CoroutineScope를 활용해서 코루틴 스코프를 만들어 낸다.
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 내부에서만 사용 할 수 있어서 사용한거야.
}
-launch{}로부터 반환되는 값으로 코루틴의 작업을 관리와 수명주기 관리를하는 역할을 수행 할 수 있다.
-Job과 관련된 메서드 및 프로퍼티는 다음과 같다.
-이처럼 Job과 관련된 메서드 및 프로퍼티를 활용하면 효율적으로 코루틴을 사용 할 수 있다.
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!")
}
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!")
}
-코루틴에서의 취소는 협동적으로 동작하기 때문에 코루틴을 취소하고 한다면 CoroutineScope 내부에서 실제로 취소 요청이 있었는지 없었는지를 확인해야 한다.
-따라서 코루틴을 완벽하게 취소하고 싶다면 [1]suspend function 사용 : 취소 시키려는 코루틴이 실행되는 동안은 스레드가 다른 코루틴을 실행시키려 자리 비울 수 있는 시간을 주어야 한다는 것이야. [2]명시적으로 isActivie 활용 이 두가지 방법을 활용해서 스레드에게 해당 코루틴이 취소되었다고 분명히 알려주어야 한다.
-코루틴이 취소되었을 때 만약 해당 코루틴에 할당된 리소스가 있다면 반드시 해제 해줘야한다. 코루틴이 .cancel()을 통해서 취소되면 CancellationException이 발생되기 때문에 , try~catch~finally 이렇게 대응을 해줘야 한다. catch에서 e를 잡아서 해당 코루틴에 할당된 자원을 해제해줘야 한다. 안전하게 해제하고 싶으면 finally 절에서 할당된 자원을 해제해주면 된다.
-특정 Coroutine은 취소가 불가능하기도 한데 이는 withContext(NonCancellable)을 활용해서 만들 수 있다.
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()
}
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()
}
> 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()
}
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()
}
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!")
}
-일정한 시간이 지난 후 Coroutine을 종료하고 싶을 때 사용한다.
-이렇게 만든 Coroutine이 일정한 시간이 지난 후 종료되면 TimeoutCancellationException이 발생한다.
-TimeoutCancellationException 없이 사용하고 싶으면 withTimeoutOrNull()을 사용하면 된다. 이는 일정한 시간이 지나면 null을 반환한다.
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()
}
}
// 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란, Coroutine의 작동 환경을 정의하고, 여러 CoroutineElement들을 조합하여 만들 수 있다. 즉, 여러 CoroutineElement 들을 조합해서 사용자가 원하는 Coroutine의 동작 방식을 구성 할 수 있다.
-CoroutineContext 요소 1 Coroutine Name : 코루틴의 이름 정해주기
-CoroutineContext 요소 2 Job : 코루틴 동작관리 + 코루틴 생명주기 관리
-CoroutineContext 요소 3 Dispatchers : 코루틴이 실행 될 스레드 관리
-CoroutineContext 요소 4 CEH : 코루틴에서 발생할 수 있는 예외 처리
-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에서 하지만 앞으로는 어디에서 수행 될 지 모른다.(문제야..비추)
@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}")
}
}
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}")
}
}
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}")
}
}
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)
}
@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(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 문으로 에러처리를 다루는 것보다 관리하기가 편하다는 이점이 존재한다.
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를 호출하지 않기 때문이다.
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()
}
-일반적인 Job 객체는 예외가 발생되면 취소 요청을 위아래로 보내게 된다. 따라서 하위 코루틴에서 에러가 발생하면 위로 올라가고 내려가고 아주 난리가 난다. 따라서 최상위 코루틴이 취소가 되면 그 아래 자식들이 전부 다 취소가 되는 문제가 존재하였다.
-그래서 이러한 문제를 해결하기 위한 SupervisorJob이라는 것이 등장하였고, 이는 예외에 의한 취소가 전달되는 방향이 오직 아랫방향뿐이다.
-SupervisorJob 또한 결국에는 Job을 상속받았기 때문에 CoroutineContext에 더해 질 수 있어서, SupervisorJob을 사용하고 싶어하는 코루틴 빌더 또는 CoroutineScope의 Parameter에 넣어주면된다.
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는 suspendFunction이며, 이를 사용 할 때에 주의사항은 반드시 코드 블락 내부에서 에러가 발생할 코루틴의 Context에 CEH 처리를 해주거나 try~catch~finally 처리를 해주어야한다. 아무리 supervisorScope 이더라도 이러한 예외 처리를 해주지 않으면 에러가 위 아래로 퍼지게 된다.
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()
}
-여러 thread를 코루틴이 사용하기 때문에 가시성 , 동시성 문제가 발생한다. 객체의 값을 인식하는 것이 코루틴들마다 다를 수 있는 문제가 있기 때문에 SharedObject , Mutex , Actor 등을 활용하면 동시성 문제를 해결 할 수 있다.
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")
}
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}")
}
// 모든 문제에 적용되지 않는다.
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")
}
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(MutualExclusion) : 상호배제의 줄임말이다. 이를 활용하면 공유 상태를 수정할 때 임계 영역(critical section)을 이용하게 하며 , 임계 영역은 스레드가 동시에 접근하는 것을 허용하지 않는다.
-val mutex = Mutex() 이렇게 임계영역을 만든 후 mutex.withLock {}을 활용해서 접근을 허용하는 코드를 작성한 후 코드 내부에서 필요한 로직을 작성하면 된다.
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를 사용하는 과정[1.sealed class 생성][2.실제 Actor 생성]
-Actor를 만드는 방법은 1.actor 함수호출 2.블록 내에 우리가 원하는 로직을 설정(상태값을 캡슐화) , 외부에서 Actor로 보내는 것은 채널을 통해서만 받을 수 있어.
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
}