-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
2부 코틀린 코루틴 라이브러리 - 1 #2
Comments
6. 코루틴 빌더launch 빌더현업에서는 fun main() {
GlobalScope.launch {
delay(1000L)
println("Text")
}
} 위처럼
Kotlin Coroutines 1.5.0 버전 이후로 fun main() {
GlobalScope.launch {
getDataFromOpenApi()
}
} 만약 서버의 성능이 좋지 않아 네트워크가 느릴 경우 백그라운드에서 계속 대기하면서 리소스를 소비하게 된다. 즉, 중단되거나 지연될 경우에도 작동을 멈추지 않는다는 것이다. 또한 7. 코루틴 컨텍스트중단 함수에서 컨텍스트에 접근하기아래 코드를 실행하면 다음과 같은 결과가 나온다. suspend fun main() = withContext(CoroutineName("Outer")){
printName()
launch(CoroutineName("Inner")) {
printName()
}
delay(10)
printName()
}
그렇다면 만약
8. 잡과 자식 코루틴 기다리기자식은 어떻게 부모 컨텍스트를 상속 받을까? fun main(): Unit = runBlocking(CoroutineName("main")) {
val name = coroutineContext[CoroutineName]?.name
println(name)
launch {
delay(1000)
val childContextName = coroutineContext[CoroutineName]?.name
println(childContextName)
}
} 위 코드를 디컴파일하면 다음과 같은 바이트 코드가 나온다. ...
BuildersKt.runBlocking(
(CoroutineContext)(new CoroutineName("main")),
(Function2)(new Function2((Continuation)null)
{
// launch 블럭
BuildersKt.launch$default(
$this$runBlocking,
(CoroutineContext)null,
(CoroutineStart)null,
(Function2)(new Function2((Continuation)null) {
CoroutineName var10000 =
(CoroutineName)$this$launch.getCoroutineContext()
.get((CoroutineContext.Key)CoroutineName.Key);
String childContextName = var10000 != null ? var10000.getName() : null;
System.out.println(childContextName);
return Unit.INSTANCE;
}
} 위처럼 만약 부모 컨텍스트가 아닌 다른 컨텍스트를 사용할 경우 다음과 같이 BuildersKt.runBlocking(
(CoroutineContext)(new CoroutineName("main")),
(Function2)(new Function2((Continuation)null)
{
BuildersKt.launch$default(
$this$runBlocking,
(CoroutineContext)(new CoroutineName("child")),
(CoroutineStart)null,
(Function2)(new Function2((Continuation)null)
{
// ...
}
} 9. 취소경쟁 상태(race condition)경쟁 상태는 말 그대로 경쟁을 하는 상태인 것이다. 멀티 스레드 환경에서 공유 자원에 접근해 상태를 변경할 때 발생한다. suspend fun CoroutineScope.massiveRun(action: suspend () -> Unit) {
val n = 100
val k = 1000
val time = measureTimeMillis {
val jobs = List(n) {
launch { repeat(k) { action() } }
}
jobs.forEach { it.join() }
}
println("Completed ${n * k} actions in $time ms")
}
var counter = 0
fun main() = runBlocking {
GlobalScope.massiveRun {
counter++
}
println("Counter = $counter")
}
위와 같이 원하는 결과가 아닌 전혀 다른 결과가 나왔다. 코드를 보면 정확히 100,000개의 우선,
이는 한 스레드가 변수를 읽고 증가하기 전에 다른 스레드가 이미 값을 변경해 발생하는 것이다. 이렇게 경쟁 상태(race condition)이 발생하게 되면서 동시성 이슈가 발생하게 된다. 이를 해결하는 가장 간단한 방법은 suspend fun massiveRun(action: suspend () -> Unit) {
coroutineScope {
val n = 100
val k = 1000
val time = measureTimeMillis {
val jobs = List(n) {
launch { repeat(k) { action() } }
}
jobs.forEach { it.join() }
}
println("Completed ${n * k} actions in $time ms")
}
}
var counter = 0
fun main() = runBlocking {
massiveRun { counter++ }
println("Counter = $counter")
}
|
Job은 코루틴을 취소하고 상태를 파악하는 등의 역할을 한다는데 그 동작 과정에 대해 알아보자. Job이란?코루틴의 실행 상태를 나타내는 인터페이스이다. 각 코루틴은 백그라운드에서 실행될 작업을 나타내는 Job을 가지고 있다. Job은 코루틴의 생명주기와 관련이 있으며, 주로 코루틴의 실행을 추적하고 관리하는 데 사용된다. 아래의 빌더로 잡을 생성하는 간단한 예제를 통해 Job의 동작 과정에 대해 알아보자. suspend fun main() = coroutineScope {
val job = Job()
println("before complete job : $job")
job.complete()
println("after complete job : $job")
}
// Job.kt
public fun Job(parent: Job? = null): CompletableJob = JobImpl(parent)
// JobSupport.kt
internal open class JobImpl(parent: Job?) : JobSupport(true), CompletableJob {
init { initParentJob(parent) }
// ...
}
// JobSupport.kt
protected fun initParentJob(parent: Job?) {
assert { parentHandle == null }
if (parent == null) { // parent 가 없을 경우
parentHandle = NonDisposableHandle
return
}
parent.start() // make sure the parent is started
@Suppress("DEPRECATION")
val handle = parent.attachChild(this) // 자신의 job을 부모의 child로 붙인다 -> 트리 구조가 형성
parentHandle = handle
if (isCompleted) {
handle.dispose()
parentHandle = NonDisposableHandle
}
}
// JobSupport.kt
public final override fun attachChild(child: ChildJob): ChildHandle {
return invokeOnCompletion(onCancelling = true, handler = ChildHandleNode(child).asHandler) as ChildHandle
}
만약 예제 코드에서 Job을 생성할 때 부모 Job을 넘긴다면 suspend fun main() = coroutineScope {
val job = Job(coroutineContext[Job])
println("before complete job : $job")
job.complete()
println("after complete job : $job")
} Job의 트리가 어떤 과정을 통해 구성되는지 알겠는데 이 Job이 코루틴에서 어떻게 쓰이는 걸까?? 코루틴 빌더는 부모의 잡을 기초로 자신들의 잡을 생성한다.모든 코루틴은 자신만의 Job을 생성하며 인자 또는 부모 코루틴으로 부터 온 잡은 새로운 잡의 부모로 사용된다. 부모와 자식 관계에 있다면 부모가 자식 코루틴을 기다리게 되는 것이다. 그렇다면 코루틴 빌더는 어떤식으로 구현이 되어있기에 Job을 기다리는 걸까..? 내부 동작 방식을 알아보자. coroutine builder코틀린에서 제공하는 primitive coroutine builder에는 크게 3가지가 있는데, 아래와 같이 분류할 수 있다.
CoroutineScope의 extension function - launchlaunch 와 async는 비슷하므로 launch의 구현을 확인해보자. // Builders.common.kt
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
// jvmMain/CoroutineContext.kt
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
val combined = coroutineContext + context
val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
debug + Dispatchers.Default else debug
}
// Builders.common.kt
private open class StandaloneCoroutine(
parentContext: CoroutineContext,
active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) {
override fun handleJobException(exception: Throwable): Boolean {
handleCoroutineException(context, exception)
return true
}
}
// Builders.common.kt
public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
start(block, receiver, this)
}
suspending function - coroutineScope다음으로 suspending function 중 하나인 coroutineScope 동작 과정에 대해 살펴보자. public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont -> // uCont : 부모 coroutine
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
internal open class ScopeCoroutine<in T>(
context: CoroutineContext,
@JvmField val uCont: Continuation<T> // unintercepted continuation
) : AbstractCoroutine<T>(context, true, true), CoroutineStackFrame {
// ...
}
|
해결못한 것들
|
CoroutineScope.launch
launch블럭을 보면 이렇게 해석이 되는데 여기서 왜 StandAloneCoroutine
2번의 내용을 보면 새로운 Coroutine을 만드는데 이때 만들어지는 것은 StandAloneCoroutine 이고 이것은 AbstractCoroutine
그럼 모든 launch / async 같은 경우 Context로 Job을 넘기는 것이 무의미한 것이 아닌가? 라는 의문이 드는데 그것은 아래 코드로 정리가 될 것 같다.
context가 새롭게 생성되기 전인 init 부분에서 ![]() parentJob의 job을 보면 SupervisorJob이 등록되어 있는 모습을 확인할 수 있다. 그렇기에 내가 정리위 내용에서 중요한 부분만 정리하자면 아래와 같다.
|
The text was updated successfully, but these errors were encountered: