728x90
- async 코루틴 빌더 뒤에 await 를 호출하지 마세요
- async 로 비동기 작업을 정의한 뒤, 아무것도 하지 않은 채 연산이 완료되는 걸 기다리는건 아무 의미가 없습니다.
// 이렇게 구현하지 마세요.
suspend fun getUser(): User = coroutineScope {
val user = async { repo.getUser() }.await()
user.toUser()
}
// 이렇게 구현하세요.
suspend fun getUser(): User {
val user = repo.getUser()
return user.toUser()
}
- withContext(EmptyCoroutineContext) 대신 coroutineScope 를 사용하세요
- withContext 와 coroutineScope 의 차이는 withContext 가 컨텍스트를 재정의 할 수 있다는 것밖에 없으므로,
withContext(EmptyCoroutineContext) 대신 coroutineScope 를 사용하세요 -
더보기withContextwithContext는 코루틴의 컨텍스트를 임시적으로 변경하고자 할 때 사용됩니다. 이 함수는 코루틴의 실행 컨텍스트(예: 디스패처)를 변경하면서 코드 블록을 실행하고, 코드 블록이 완료되면 원래 컨텍스트로 돌아옵니다.withContext는 결과 값을 반환할 수 있으며, 호출된 스코프의 코루틴이 중단될 때까지 대기합니다. 따라서 비동기 작업의 결과를 기다리고 그 결과를 반환하고자 할 때 유용합니다.예: withContext(Dispatchers.IO) { ... }는 I/O 작업을 위해 디스패처를 임시로 변경합니다.coroutineScopecoroutineScope는 새로운 코루틴 스코프를 생성하지 않습니다. 대신 현재 코루틴의 스코프 내에서 새로운 코루틴을 시작할 수 있는 블록을 제공합니다. 이 블록 내에서 시작된 모든 코루틴이 완료될 때까지 coroutineScope는 중단됩니다.coroutineScope는 자식 코루틴들이 모두 완료될 때까지 현재 코루틴의 실행을 중단시키므로, 여러 비동기 작업을 병렬로 실행하고 모두 완료될 때까지 기다리고자 할 때 사용됩니다.coroutineScope는 현재 컨텍스트를 변경하지 않습니다.주요 차이점컨텍스트 변경: withContext는 코루틴의 실행 컨텍스트를 임시적으로 변경할 수 있지만, coroutineScope는 현재 컨텍스트를 유지합니다.용도: withContext는 주로 컨텍스트를 변경하면서 결과 값을 반환하고 싶을 때 사용되고, coroutineScope는 현재 스코프 내에서 여러 비동기 작업을 관리하고 싶을 때 사용됩니다.withContext(EmptyCoroutineContext) 대신 coroutineScope 사용 권장withContext(EmptyCoroutineContext)를 사용하면 컨텍스트를 변경하지 않는 효과가 있으므로, 이 경우 coroutineScope를 사용하는 것이 더 적절합니다. withContext(EmptyCoroutineContext)는 컨텍스트 변경의 의도가 없을 때 혼란을 줄 수 있으며, coroutineScope는 이런 경우에 의도를 더 명확하게 표현합니다. coroutineScope는 현재 컨텍스트를 유지하면서 새로운 코루틴을 시작할 수 있으므로, 컨텍스트 변경 없이 여러 비동기 작업을 동시에 실행하고 싶을 때 사용하기에 적합합니다.
- withContext 와 coroutineScope 의 차이는 withContext 가 컨텍스트를 재정의 할 수 있다는 것밖에 없으므로,
- awaitAll 을 사용하세요
- awaitAll 함수는 비동기 작업 중 하나가 예외를 던졌을 경우에 곧바로 멈추며, map {it.await()} 는 실패하는 작업에 맞닥뜨릴 때까지 하나씩 기다리므로 map {it.await()} 보다 awaitAll 을 사용해야 합니다.
- awaitAll 함수는 여러 비동기 작업(Deferred 객체)을 동시에 기다리는 데 사용됩니다. 이 함수는 모든 작업이 성공적으로 완료될 때까지 기다리고, 결과를 모아 반환합니다. 하지만, 이 작업 중 하나라도 예외를 던지면, awaitAll은 즉시 실패하며, 나머지 작업의 완료를 기다리지 않습니다. 이 접근 방식은 모든 작업이 동시에 실행되며, 첫 번째 예외가 발생하는 즉시 처리가 중단된다는 장점이 있습니다.
- 반면에, map { it.await() }를 사용하면, Deferred 객체의 리스트를 순회하며 각각을 차례로 기다립니다. 이 방식은 각 작업이 순차적으로 완료될 때까지 기다리므로, 만약 어느 하나의 작업에서 예외가 발생하면, 그 시점까지 완료된 작업들의 결과만 사용할 수 있습니다. 이는 awaitAll보다 비효율적일 수 있으며, 모든 작업이 병렬로 실행되더라도 실패한 작업에 도달할 때까지 기다린다는 단점이 있습니다.
- awaitAll 함수는 비동기 작업 중 하나가 예외를 던졌을 경우에 곧바로 멈추며, map {it.await()} 는 실패하는 작업에 맞닥뜨릴 때까지 하나씩 기다리므로 map {it.await()} 보다 awaitAll 을 사용해야 합니다.
fun main30_1() = runBlocking {
val deferredList = listOf(
async { delay(1000); println("Task 1 completed"); 1 },
async { delay(500); throw RuntimeException("Error in Task 2"); 2 },
async { delay(100); println("Task 3 completed"); 3 }
)
try {
val results = deferredList.awaitAll() // 모든 작업을 동시에 기다림
println("Results: $results")
} catch (e: Exception) {
println("Caught an exception: ${e.message}")
}
val individualResults = mutableListOf<Int>()
try {
deferredList.map {
try {
individualResults.add(it.await()) // 각 작업을 차례로 기다림
} catch (e: Exception) {
println("Caught an exception in map: ${e.message}")
}
}
println("Individual Results: $individualResults")
} catch (e: Exception) {
println("Caught an exception: ${e.message}")
}
}
- 중단 함수는 어떤 스레드에서 호출되어도 안전해야 합니다.
- 중단 함수를 호출할 때, 지금 사용하고 있는 스레드가 블로킹될까 봐 걱정하면 안 됩니다.
- 중단 함수가 블로킹 함수를 호출할 때는 Dispatchers.IO 나 블로킹에 사용하기로 설계된 커스텀 디스패처를 사용해야 합니다.
- CPU 집약적인 디스패처로는 Dispatchers.Defaul 나 병렬 처리 수가 제한된 Dispatchers.Defaul 를 사용 해야 합니다.
- 함수를 호출할 때 디스패처를 설정할 필요가 없도록 withContext 로 디스패처를 설정해야 합니다.
suspend fun fetchDataFromNetwork(): String {
// 네트워크 호출을 가정한 IO 작업
return withContext(Dispatchers.IO) {
// 네트워크 요청을 여기서 수행
"데이터"
}
}
suspend fun processData(data: String): String {
// 데이터 처리를 가정한 CPU 집약적 작업
return withContext(Dispatchers.Default) {
// 데이터 처리 로직
data.reversed() // 예시로, 데이터를 뒤집는 작업을 수행
}
}
fun main30_2() = runBlocking {
val data = fetchDataFromNetwork()
val processedData = processData(data)
println(processedData)
}
- Dispatchers.Main 대신 Dispatchers.Main.immediate 를 사용하세요
- Dispatchers.Main.immediate 는 Dispatchers.Main 이 최적화된 것으로 필요한 경우에만 코루틴을 재분배 합니다.
- 보통은 Dispatchers.Main.immediate 를 사용 합니다
- 무거운 함수에서는 yield 를 사용하는 것을 기억하세요
- 중단 가능하지 않으면서 CPU 집약적인 또는 시간 집약적인 연산들이 중단 함수에 있다면, 각 연산들 사이에 yield 를 사용하는 것이 좋습니다
suspend fun performLongRunningTask() {
for (i in 1..100) {
// 시간이 많이 소요되는 연산 수행
heavyCalculation(i)
// 다른 코루틴에게 실행을 양보
yield()
}
}
suspend fun performLongRunningTask2() {
for (i in 1..100) {
// 시간이 많이 소요되는 연산 수행
heavyCalculation(i)
// 다른 코루틴에게 실행을 양보
yield()
}
}
suspend fun heavyCalculation(i: Int) {
// 복잡한 계산 수행 (예시)
println("i: $i")
Thread.sleep(10) // 대체로, 실제 애플리케이션에서는 Thread.sleep 대신 실제 계산을 수행
}
fun main30_3() = runBlocking {
launch {
performLongRunningTask()
}
launch {
performLongRunningTask2()
}
// 다른 코루틴 작업을 여기에 추가할 수 있습니다.
}
i: 1
i: 1
i: 2
i: 2
i: 3
i: 3
i: 4
i: 4
i: 5
i: 5
i: 6
i: 6
i: 7
....
- 중단 함수는 자식 코루틴이 완료되는 걸 기다립니다.
- 부모 코루틴은 자식 코루틴이 끝나기 전에 완료될 수 없으며, coroutineScope 나 withContext 같은 코루틴 스코프 함수는 스코프 내의 코루틴이 완료될 때까지 부모 코루틴을 중단시킵니다.
- 그 결과, 부모 코루틴은 부모 코루틴이 시작한 모든 코루틴을 기다리게 됩니다.
suspend fun longTask() = coroutineScope {
launch {
delay(1000)
println("Done 1")
}
launch {
delay(2000)
println("Done 2")
}
}
fun main30_4() = runBlocking {
println("Before")
longTask()
println("After")
}
Before
-- 1초 후
Done 1
-- 2초 후
Done 2
After
- 코루틴 스코프 함수의 마지막 코드에 launch 를 사용하면 launch 를 제거해도 똑같기 때문에 굳이 launch 를 사용할 필요가 없습니다.
- 중단 함수는 함수 내에서 시작한 코루틴이 완료되는 걸 기다리는 것이 기본 동작입니다. 외부 스코프를 사용하면 이 원칙을 위배할 수 있으며 합당한 이유가 없다면 이런 방식은 피해야 합니다.
- Job 은 상속되지 않으며, 부모 관계를 위해 사용됩니다.
- Job 은 유일하게 상속되지 않는 컨텍스트입니다.
- 코루틴은 각자의 잡을 가지고 있으며, 잡을 자식 코루틴으로 전달하고, 전달된 잡이 자식 코루틴 잡의 부모가 됩니다.
// 이렇게 구현하면 안 됩니다.
fun main30_5() = runBlocking(SupervisorJob()) {
launch {
delay(1000)
throw Error()
}
launch {
delay(1000)
println("Done")
}
launch {
delay(3000)
println("Done")
}
}
- 구조화된 동시성을 깨뜨리지 마세요.
- Job 을 명시적인 코루틴 컨텍스트로 설정했을 경우, 코루틴의 부모 관계가 깨질 수 있습니다.
- Job 을 코루틴의 인자로 사용할 경우 Job 이 코루틴의 부모로 설정되는 문제가 발생합니다.
- CoroutineScope 를 만들 때는 SupervisorJob 을 사용하세요
- 스코프를 만들 때, 보통 스코프에서 시작한 코루틴에서 예외가 발생하면 다른 모든 코루틴을 취소하지 않을 거라고 생각합니다.
- 예외가 전파되지 않는 스코프를 만들려면 디폴트로 설정된 Job 대신에 SupervisorJob 을 사용해야 합니다.
- Job: 기본적으로, 코루틴 스코프는 Job 객체를 사용하여 스코프 내의 코루틴들을 관리합니다. 스코프 내의 한 코루틴에서 예외가 발생하면, 해당 예외는 스코프 내의 모든 코루틴을 취소하는데, 이는 모든 자식 코루틴들이 함께 실패하도록 보장합니다. 이는 코루틴 간의 강한 결합과 오류 전파를 의미합니다.
- SupervisorJob: 반면, SupervisorJob을 사용하면 코루틴 스코프 내에서 예외의 전파 방식이 달라집니다. 스코프 내의 한 코루틴에서 예외가 발생해도, SupervisorJob은 이 예외가 다른 코루틴으로 전파되어 나머지 코루틴들을 취소하지 않습니다. 즉, 하나의 코루틴 실패가 전체 스코프의 다른 코루틴들에 영향을 미치지 않도록 합니다.
// Job 으로..
fun main30_6() = runBlocking {
val scopeWithJob = CoroutineScope(Job() + Dispatchers.IO)
scopeWithJob.launch {
println("Task 1 is starting")
throw Exception("Error in Task 1")
}
scopeWithJob.launch {
delay(100) // Task 1에서 예외가 발생하면 이 작업은 취소됩니다.
println("Task 2 is running") // 이 메시지는 출력되지 않습니다.
}
delay(500) // 충분한 실행 시간을 주기
}
Task 1 is starting
Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" ...
.....
// SupervisorJob 으로..
fun main30_7() = runBlocking {
val scopeWithSupervisorJob = CoroutineScope(SupervisorJob() + Dispatchers.IO)
scopeWithSupervisorJob.launch {
println("Task 1 is starting")
throw Exception("Error in Task 1") // 이 예외는 이 코루틴에만 영향을 미칩니다.
}
scopeWithSupervisorJob.launch {
delay(100) // Task 1에서 예외가 발생해도, 이 작업은 계속 실행됩니다.
println("Task 2 is running") // 이 메시지는 정상적으로 출력됩니다.
}
delay(500) // 충분한 실행 시간을 주기
}
Task 1 is starting
Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.Exception: Error in Task 1
...
Task 2 is running
- 스코프의 자식은 취소할 수 있습니다.
- 스코프가 취소되고 나면, 취소된 스코프를 다시 사용할 수 없습니다.
- 스코프에서 시작한 모든 작업을 취소하지만 스코프를 액티브 상태로 유지하고 싶은 경우에는, 스코프의 자식을 취소하면 됩니다.
- 스코프를 유지하는 건 아무런 비용이 들지 않습니다.
fun main30_8() = runBlocking {
val scope = CoroutineScope(Dispatchers.Default)
// 자식 코루틴 1
val childJob1 = scope.launch {
delay(1000)
println("Child coroutine 1 completed.")
}
// 자식 코루틴 2
val childJob2 = scope.launch {
delay(2000)
println("Child coroutine 2 completed.")
}
// 특정 조건에서 첫 번째 자식 코루틴만 취소
println("Cancelling child coroutine 1...")
childJob1.cancel() // 첫 번째 자식 코루틴 취소
// 나머지 작업이 완료되기를 기다림
childJob2.join() // 두 번째 코루틴의 완료를 기다림
println("Main coroutine is completed.")
}
Cancelling child coroutine 1...
Child coroutine 2 completed.
Main coroutine is completed.
- GlobalScope 를 사용하지 마세요
- 사용하기는 쉽지만 SupervisorJob 컨텍스트로 아주 간단한 스코프를 만드는게 낫다.
- GlobalScope 는 관계가 없으며, 취소도 할 수 없고, 테스트를 위해 오버라이딩 하는 것도 힘듭니다.
- GlobalScope 가 당장 필요하더라도, 의미 있는 스코프를 만드는 것이 나중에 도움이 될 것임.
- 스코프를 만들 때를 제외하고, Job 빌더를 사용하지 마세요.
- Job 함수를 사용해 잡을 생성하면, 자식의 상태와는 상관없이 액티브 상태로 생성된다.
- 자식 코루틴 일부가 완료되더라도, 부모 또한 완료되는 것은 아닙니다.
// 이렇게 하지 마세요
fun main30_9(): Unit = runBlocking {
val job = Job()
launch(job) {
delay(1000)
println("Text 1")
}
launch(job) {
delay(2000)
println("Text 2")
}
job.join() // 여기서 영원히 대기하게 된다.
println("Will not be prointed")
}
Text 1
Text 2
-- 영원히 대기
// 이렇게 해라
-------------------------------------------------
fun main30_10() = runBlocking {
val job1 = launch {
delay(1000)
println("Text 1")
}
val job2 = launch {
delay(2000)
println("Text 2")
}
// 각 코루틴의 완료를 기다립니다.
job1.join()
job2.join()
println("Both coroutines have completed")
}
Text 1
Text 2
Both coroutines have completed
- Job 이 완료되려면, complete 메서드가 먼저 호출되고 'Active' 상태에서 자식 코루틴이 종료될 때까지 기다리는 'Completing' 상태로 바뀌어야 합니다.
- 하지만 완료 중이거나 완료된 잡에서 새로운 코루틴을 시작할 수 없습니다.
- 잡의 자식 참조를 사용해 자식 코루틴을 기다리는 것이 좀더 실용적입니다.
728x90
'공부 > 코틀린코루틴:딥다이브(마르친 모스카와)' 카테고리의 다른 글
29장 - 코루틴을 시작하는 것과 중단 함 숮 어떤 것이 나을까? (0) | 2024.03.16 |
---|---|
18장 - 핫 데이터 소스와 콜드 데이터 소스 (0) | 2024.03.10 |
16장 - 채널 (1) | 2024.03.02 |
14장 - 공유 상태로 인한 문제 (0) | 2024.03.01 |
13장 - 코루틴 스코프 만들기 (0) | 2024.02.26 |
댓글