CoroutineScope は、コルーチンの構造化された並行性を実現するための鍵です。CoroutineScope を使用すると、すべてのコルーチンを同じスコープで一括管理できます。
CoroutineScope と構造化された同時実行性
launch と async は、CoroutineScope の拡張機能として定義されています。launch を呼び出す前に CoroutineScope を取得する必要があります。
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
}
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
なぜ拡張メソッドとして設計する必要があるのですか?
fun main() {
testCoroutinueScope()
}
private fun showScopeLog(any: Any?) {
println("""$any Thread:${Thread.currentThread().name}""".trimIndent())
}
private fun testCoroutinueScope() {
val scope = CoroutineScope(Job())
scope.launch {
launch {
delay(1000000L)
showScopeLog("Inner")
}
showScopeLog("Hello")
delay(1000000L)
showScopeLog("World") //不执行
}
scope.launch {
launch {
delay(1000000L)
showScopeLog("Inner!!!")
}
showScopeLog("Hello!!!")
delay(1000000L)
showScopeLog("World!!!") //不执行
}
Thread.sleep(500L)
scope.cancel()
}
Log:
Hello Thread:DefaultDispatcher-worker-1 @coroutine#1
Hello!!! Thread:DefaultDispatcher-worker-3 @coroutine#2
Process finished with exit code 0
scope は 2 つのトップレベル コルーチンを作成し、コルーチン内で launch を使用してサブ コルーチンを作成します。最後に、コルーチンの外で 500 ミリ秒待機し、scope.cancel() が呼び出されます。その結果、以前に作成された 4 つのコルーチンはすべてキャンセルされます。
親コルーチンは Scope に属し、子コルーチンは親コルーチンに属し、scope.cancel() が呼び出される限り、この 4 つのコルーチンはキャンセルされます。
コルーチンを管理する CoroutineScope の機能は Job に由来します。
親子コルーチン関係を確立するには? - CoroutineScope がジョブを通じてコルーチンを管理する方法。
CoroutineScope はインターフェイスですが、なぜそのコンストラクターを呼び出して CoroutineScope オブジェクトを作成できるのでしょうか? 匿名の内部クラスを作成するために object キーワードを使用すべきではありませんか?
CoroutineScope() の呼び出しはコンストラクターではなく、トップレベルの関数です。
private fun testCoroutinueScope() {
val scope = CoroutineScope(Job())
scope.launch {
launch {
delay(1000000L)
showScopeLog("Inner")
}
showScopeLog("Hello")
delay(1000000L)
showScopeLog("World") //不执行
}
scope.launch {
launch {
delay(1000000L)
showScopeLog("Inner!!!")
}
showScopeLog("Hello!!!")
delay(1000000L)
showScopeLog("World!!!") //不执行
}
Thread.sleep(500L)
scope.cancel()
}
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
internal class ContextScope(context: CoroutineContext) : CoroutineScope {
override val coroutineContext: CoroutineContext = context
// CoroutineScope is used intentionally for user-friendly representation
override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)"
}
public fun Job(parent: Job? = null): CompletableJob = JobImpl(parent)
Kotlin での関数名は、ほとんどの場合「キャメル ケース命名法」に従いますが、一部の特殊なケースではこの命名法に従わない場合があります。上記のトップレベル関数 CoroutineScope() は実際には特殊なケースです。これは通常のトップレベル関数ですが、その関数は「コンストラクター」であるためです。同様の使い方で、最上位関数 Job() もあります。
Kotlin では、トップレベルの関数をコンストラクターとして使用する場合、最初の文字が大文字になります。
CoroutineScope を作成するとき、受信した Context に Job が含まれている場合はそれを直接使用し、Job が含まれていない場合は新しい Job が作成されます。これは、すべての CoroutineScope オブジェクトの Context に Job オブジェクトが必要であることを意味します。コード内の CoroutineScope(Job()) を CoroutineScope() に変更しても問題ありません。
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
}
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
}
}
private class LazyStandaloneCoroutine(
parentContext: CoroutineContext,
block: suspend CoroutineScope.() -> Unit
) : StandaloneCoroutine(parentContext, active = false) {
private val continuation = block.createCoroutineUnintercepted(this, this)
override fun onStart() {
continuation.startCoroutineCancellable(this)
}
}
StandaloneCoroutine は、コルーチンの抽象クラスを表す AbstractCoroutine のサブクラスです。さらに、initParentJob パラメーターがあり、これは true です。これは、コルーチンの作成後に、コルーチンの親子関係を初期化する必要があることを意味します。LazyStandaloneCoroutine は StandaloneCoroutine のサブクラスであり、その active パラメーターは false です。これは、遅延読み込み方式でコルーチンを作成することを意味します。
public abstract class AbstractCoroutine<in T>(
parentContext: CoroutineContext,
initParentJob: Boolean,
active: Boolean
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {
... ...
init {
/*
* Setup parent-child relationship between the parent in the context and the current coroutine.
* It may cause this coroutine to become _cancelling_ if the parent is already cancelled.
* It is dangerous to install parent-child relationship here if the coroutine class
* operates its state from within onCancelled or onCancelling
* (with exceptions for rx integrations that can't have any parent)
*/
if (initParentJob) initParentJob(parentContext[Job])
}
}
AbstractCoroutine は JobSupport のサブクラスであり、init{} コード ブロックでは、initParentJob パラメータに従って、コルーチンの親子関係を初期化する必要があるかどうかが判断されます。initParentJob が true であるため、ここの initParentJob() メソッドは確実に実行され、そのパラメーターparentContext[Job] によって取り出されたジョブは、実際にはスコープ内のジョブです。
initParentJob() メソッドは、その親クラス JobSupport のメソッドです。
public open class JobSupport constructor(active: Boolean) : Job, ChildJob, ParentJob, SelectClause0 {
final override val key: CoroutineContext.Key<*> get() = Job
protected fun initParentJob(parent: Job?) {
assert { parentHandle == null }
if (parent == null) {
parentHandle = NonDisposableHandle
return
}
parent.start()
@Suppress("DEPRECATION")
val handle = parent.attachChild(this)
parentHandle = handle
if (isCompleted) {
handle.dispose()
parentHandle = NonDisposableHandle
}
}
}
public interface Job : CoroutineContext.Element {
public val children: Sequence<Job>
public fun attachChild(child: ChildJob): ChildHandle
}
上記のコードには、注目すべき 3 つの場所があります。分析してみましょう。
まず、渡された親が空かどうかを判断し、親が空の場合、現在のコルーチンには親ジョブがなく、コルーチンの親子関係を作成する必要がないことを意味します。
次に、親に対応するジョブが開始されていることを確認します。
parent.attachChild(this) は、現在のジョブを親の子ジョブとして追加します。これは実際には、コルーチンの親子関係を確立するためのキー コードです。
コルーチンの「構造化キャンセル」はどうですか?
コルーチンの構造化されたキャンセルは、本質的にイベントの配信です。
public fun CoroutineScope.cancel(cause: CancellationException? = null) {
val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
job.cancel(cause)
}
CoroutineScope の cancel() メソッドは、基本的にその中で Job.cancel() を呼び出します。そして、このメソッドの具体的な実装は JobSupport にあります
public override fun cancel(cause: CancellationException?) {
cancelInternal(cause ?: defaultCancellationException())
}
public open fun cancelInternal(cause: Throwable) {
cancelImpl(cause)
}
internal fun cancelImpl(cause: Any?): Boolean {
var finalState: Any? = COMPLETING_ALREADY
if (onCancelComplete) {
finalState = cancelMakeCompleting(cause)
if (finalState === COMPLETING_WAITING_CHILDREN) return true
}
if (finalState === COMPLETING_ALREADY) {
finalState = makeCancelling(cause)
}
return when {
finalState === COMPLETING_ALREADY -> true
finalState === COMPLETING_WAITING_CHILDREN -> true
finalState === TOO_LATE_TO_CANCEL -> false
else -> {
afterCompletion(finalState)
true
}
}
}
if (onCancelComplete) {
finalState = cancelMakeCompleting(cause)
if (finalState === COMPLETING_WAITING_CHILDREN)
return true
}
job.cancel() は最終的に JobSupport の cancelImpl() メソッドを呼び出します。上記のコードでは、onCancelComplete は Boolean 型のメンバー プロパティです。実行する必要があるコルーチン本体があるかどうかにかかわらず、現在のジョブを表します。また、CoroutineScope のジョブは手動で作成され、コルーチン コードを実行する必要がないため、true になります。cancelMakeCompleting() メソッドの分析を続けます。
private fun cancelMakeCompleting(cause: Any?): Any? {
loopOnState { state ->
val finalState = tryMakeCompleting(state, proposedUpdate)
if (finalState !== COMPLETING_RETRY) return finalState
}
}
private fun tryMakeCompleting(state: Any?, proposedUpdate: Any?): Any? {
if (state !is Incomplete)
return COMPLETING_ALREADY
return COMPLETING_RETRY
}
return tryMakeCompletingSlowPath(state, proposedUpdate)
}
private fun tryMakeCompletingSlowPath(state: Incomplete, proposedUpdate: Any?): Any? {
notifyRootCause?.let { notifyCancelling(list, it) }
return finalizeFinishingState(finishing, proposedUpdate)
}
cancelMakeCompleting() は tryMakeCompleting() メソッドを呼び出し、最終的には tryMakeCompletingSlowPath() で notifyCancelling() メソッドを呼び出します。したがって、これは最も重要なコードです。
private fun notifyCancelling(list: NodeList, cause: Throwable) {
onCancelling(cause)
notifyHandlers<JobCancellingNode>(list, cause)
cancelParent(cause)
}
- 子ジョブに通知
notifyHandlers<JobCancellingNode>(list, cause)
- 親ジョブに通知
cancelParent(cause)
サブジョブ プロセスに通知します。
private inline fun <reified T: JobNode> notifyHandlers(list: NodeList, cause: Throwable?) {
var exception: Throwable? = null
list.forEach<T> { node ->
try {
node.invoke(cause)
} catch (ex: Throwable) {
exception?.apply { addSuppressedThrowable(ex) } ?: run {
exception = CompletionHandlerException("Exception in completion handler $node for $this", ex)
}
}
}
exception?.let { handleOnCompletionException(it) }
}
現在のジョブのサブジョブをトラバースし、キャンセルされた原因を渡すと、ここでの invoke() は最終的に ChildHandleNode の invoke() メソッドを呼び出します。
internal class ChildHandleNode(
@JvmField val childJob: ChildJob
) : JobCancellingNode(), ChildHandle {
override val parent: Job get() = job
override fun invoke(cause: Throwable?) = childJob.parentCancelled(job)
override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
}
public final override fun parentCancelled(parentJob: ParentJob) {
cancelImpl(parentJob)
}
ChildHandleNode の invoke() メソッドは、parentCancelled() メソッドを呼び出し、最終的に cancelImpl() メソッドを呼び出します。ジョブキャンセルの入力機能。これは、実際には再帰呼び出しを行うことと同じです。
親ジョブに通知するプロセス:
private fun cancelParent(cause: Throwable): Boolean {
if (isScopedCoroutine) return true
val isCancellation = cause is CancellationException
val parent = parentHandle
if (parent === null || parent === NonDisposableHandle) {
return isCancellation
}
return parent.childCancelled(cause) || isCancellation
}
この関数の戻り値は、親コルーチンが例外を処理したことを示すために true を返し、親コルーチンが例外を処理しなかったことを示すために false を返します。この設計パターンは、責任の連鎖に似ています。
public open fun childCancelled(cause: Throwable): Boolean {
if (cause is CancellationException) return true
return cancelImpl(cause) && handlesException
}
例外が CancellationException の場合、コルーチンは特別な処理を実行します。一般に、親コルーチンは子コルーチンからの取り消し例外を無視します。それが別の例外である場合、親コルーチンは子コルーチンのキャンセルに応答します。コードは引き続き cancelImpl() メソッドを再帰的に呼び出します。