Kotlin学习笔记26 协程part6 协程与线程的关系 Dispatchers.Unconfined 协程调试 协程上下文切换 Job详解 父子协程的关系

参考链接

示例来自bilibili Kotlin语言深入解析 张龙老师的视频

1 协程与线程的关系

import kotlinx.coroutines.*
import java.util.concurrent.Executors

/**
 * 协程与线程的关系:协程依赖于线程执行
 *
 * 协程上下文:(Coroutine Context)
 * 协程总是在某个上下文中运行,这个上下文实际由CoroutineContext的一个实例来表示,该实例是由Kotlin标准库定义的
 * 协程上下文本质是各种元素的集合。主要元素包括 协程的Job 协程分发器Dispatcher
 *
 * 协程分发器:(Dispatcher)
 * CoroutineDispatcher是一个抽象类 Base class to be extended by all coroutine dispatcher implementations.
 * 所有协程分发器都应该继承这个抽象类
 *
 * 其主要功能是决定协程运行在哪个线程。
 * 协程分发器的常见case有
 * 1.分发器可以将协程的执行限制在一个具体指定的线程
 * 2.可以将协程分发到一个线程池中,由线程池中的某个线程执行协程
 * 3.不加任何限制的去执行协程的代码(在这种情况下 我们所指定的协程代码到底是由哪个线程或线程池执行是不确定的,它需要根据程序的实际执行情况确定;
 * 这种方式的协程分发器我们开发中极少使用,它只用在一些极为特殊的情况下)
 *
 * 所有的协程构建器(coroutine builder)如launch async都会接受一个可选的CoroutineContext参数,该参数可以用于显示指定协程所运行的
 * 分发器及其他上下文元素
 *
 * 背景: CoroutineContext与CoroutineDispatcher的关系
 * 首先CoroutineDispatcher是所有协程分发器的基类
 * 我们查看一个协程分发器的定义例如(Dispatchers.Default) 其类型是CoroutineDispatcher
 * CoroutineDispatcher继承自AbstractCoroutineContextElement(ContinuationInterceptor)并实现了ContinuationInterceptor
 * 注意ContinuationInterceptor实现了接口 CoroutineContext.Element
 * 而CoroutineContext.Element接口实现了CoroutineContext
 * 也就是说CoroutineDispatcher本身也是一个CoroutineContext
 * 因此 CoroutineScope.launch的第一个参数虽然是CoroutineContext类型 但是我们还是可以传入CoroutineDispatcher的类型 例如
 * Dispatchers.Unconfined Dispatchers.Default等
 *
 *
 * 示例:协程与线程的关系
 * 程序分析:
 * 1.当通过launch来启动协程并且不指定协程分发器时,他会继承启动它的那个CoroutineScope的协程上下文与分发器。对本案例,他会继承runBlocking
 * 的上下文,而runBlocking是运行在主线程中的 因此该例子协程运行在main线程
 * 2.Dispatchers.Unconfined是一种特殊的协程分发器,它在本案例中运行在main线程,但实际上,其运行机制与不指定协程分发器完全不同,日常极少
 * 使用该协程分发器 后面详细看看这种分发器 目前了解即可
 * 3.Dispatchers.Default是默认协程分发器,当协程是通过GlobalScope来启动的时候,他会使用该默认的分发器启动协程,它会使用一个后台的
 * 共享线程池来运行协程。因此,launch(Dispatchers.Default){} 等价于GlobalScope.launch {}
 * 我们查看Default的定义如下
 *
 * The default [CoroutineDispatcher] that is used by all standard builders like
 * [launch][CoroutineScope.launch], [async][CoroutineScope.async], etc
 * if no dispatcher nor any other [ContinuationInterceptor] is specified in their context.
 * 该默认协程分发器会被标准协程构建器(例如CoroutineScope.launch CoroutineScope.async等使用)
 * 这种情况下,如果协程构建器没有指定其他的ContinuationInterceptor或者协程分发器,那么他们就会使用Dispatchers.Default来创建协程
 *
 * It is backed by a shared pool of threads on JVM. By default, the maximal level of parallelism used
 * by this dispatcher is equal to the number of CPU cores, but is at least two.
 * Level of parallelism X guarantees that no more than X tasks can be executed in this dispatcher in parallel.
 * 它背后由一个JVM的共享线程池支撑。默认情况下,最大并行度等于CPU核心数,但是并行数至少有两个
 * 并行度 X 保证在这个调度器中并行执行的任务不超过 X 个
 *
 * 看完定义对于Dispatchers.Default 我们知道
 * a.CoroutineScope.launch CoroutineScope.async等协程构建器默认会使用这种协程分发器
 * b.Dispatchers.Default背后由JVM共享线程池来支撑,具体由线程池中的哪个线程执行协程 开发者不确定
 * c.最大并行度等于CPU核心数,但是并行数至少有两个
 *
 * 4.Executors.newSingleThreadExecutor().asCoroutineDispatcher() 创建一个单线程的线程池,该线程池中的线程用来执行我们所指定的
 * 协程代码;在实际开发中,使用专门的线程运行协程的代价是非常高的,因此在协程执行完毕后,我们必须释放资源,这里就需要使用close方法来关闭相应的
 * 协程分发器,从而释放资源;也可以将协程分发器存储在顶层变量中,以便在程序的其他地方进行复用
 *
 * 查看官方文档关于CoroutineDispatcher的介绍可以加深我们对于各种协程分发器的理解
 * The following standard implementations are provided by kotlinx.coroutines as properties on the Dispatchers object:
 *
 *
 * Dispatchers.Default — is used by all standard builders if no dispatcher or any other ContinuationInterceptor is
 * specified in their context. It uses a common pool of shared background threads. This is an appropriate choice for
 * compute-intensive coroutines that consume CPU resources.
 *
 * Dispatchers.IO — uses a shared pool of on-demand created threads and is designed for offloading of IO-intensive
 * blocking operations (like file I/O and blocking socket I/O).
 *
 * Dispatchers.Unconfined — starts coroutine execution in the current call-frame until the first suspension,
 * whereupon the coroutine builder function returns. The coroutine will later resume in whatever thread used by the
 * corresponding suspending function, without confining it to any specific thread or pool. The Unconfined dispatcher
 * should not normally be used in code.
 *
 * Private thread pools can be created with newSingleThreadContext and newFixedThreadPoolContext.
 * An arbitrary Executor can be converted to a dispatcher with the asCoroutineDispatcher extension function.
 * This class ensures that debugging facilities in newCoroutineContext function work properly.
 *
 */

/**
 * 使用不同的协程分发器对比
 */
fun main() = runBlocking {
    launch {
        println("No params, thread:${Thread.currentThread().name}")
        /**
         * 输出 No params, thread:main
         * 说明这种情况下 该协程运行在主线程
         */
    }

    launch(Dispatchers.Unconfined) {
        // delay(100) // 多这一句 协程就会运行在不同的线程
        println("Dispatchers.Unconfined, thread:${Thread.currentThread().name}")
        /**
         * 输出 Dispatchers.Unconfined, thread:main
         * 说明这种情况下 该协程“碰巧”运行在主线程
         */
    }

    launch(Dispatchers.Default) {
        println("Dispatchers.Default, thread:${Thread.currentThread().name}")
        /**
         * 测试的时候输出 Dispatchers.Default, thread:DefaultDispatcher-worker-2
         *
         */
    }

    // 改进写法
    val threadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
    launch(threadDispatcher) {
        println("SingleThreadExecutor, thread:${Thread.currentThread().name}")
        /**
         * close方法:
         * Closes this coroutine dispatcher and shuts down its executor.
         * It may throw an exception if this dispatcher is global and cannot be closed.
         */
        threadDispatcher.close()// 如果不加这句 程序无法退出
        /**
         * 输出 SingleThreadExecutor, thread:pool-1-thread-1
         * 说明这种情况下 该协程运行在我们创建的第一个线程池里的第一个线程
         */
    }

    // 验证GlobalScope.launch等价于launch(Dispatchers.Default)
    GlobalScope.launch {
        println("GlobalScope.launch, thread:${Thread.currentThread().name}")
    }

    println("end")


    /* asCoroutineDispatcher方法详解
     asCoroutineDispatcher是Kotlin为ExecutorService提供的扩展方法 专门用于将ExecutorService创建的线程池作为协程的分发器,即
     协程会运行在该线程池中的线程上
     asCoroutineDispatcher 返回ExecutorCoroutineDispatcher
     ExecutorCoroutineDispatcher 继承自CoroutineDispatcher
     而CoroutineDispatcher是所有协程分发器的基类
     按照这种写法 线程无法终止 程序不会停止 需要改进写法
     launch(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) {
         println("SingleThreadExecutor, thread:${Thread.currentThread().name}")
     }*/
}

class HelloKotlin1 {
}

2  Dispatchers.Unconfined

import kotlinx.coroutines.*

/**
 * Dispatchers.Unconfined详解
 * Dispatchers.Unconfined协程分发器会在调用者线程中启动协程,但是仅仅会持续到第一个挂起点;当挂起函数结束后程序恢复运行时,他会继续协程代码的
 * 执行,但是这时执行协程的线程由执行挂起函数的线程决定(挂起函数由哪个线程执行 后续协程就在这个线程运行)。
 * Dispatchers.Unconfined协程分发器适用于这样的一些协程(协程不由固定线程执行 而由任意线程执行):它既不会消耗CPU时间,同时也不会更新任何共享的数据(特定于具体的线程)
 *
 * Dispatchers.Unconfined是一种高级的机制,他对于某些特殊情况是很有帮助的:协程执行的分发是不需要的或者协程的分发会产生意料之外的副作用的,这是因为协程中的操作必须立即执行
 *
 * 日常中 我们极少使用Dispatchers.Unconfined分发器
 *
 *
 *
 * Dispatchers.Unconfined官方介绍:
 * A coroutine dispatcher that is not confined to any specific thread.
 * 一种不会指定特定线程的协程分发器
 * It executes initial continuation of the coroutine in the current call-frame
 * and lets the coroutine resume in whatever thread that is used by the corresponding suspending function, without
 * mandating any specific threading policy. Nested coroutines launched in this dispatcher form an event-loop to avoid
 * stack overflows.
 * 它会在当前调用帧中继续执行初始的协程 并且让协程继续在任何线程执行,该线程是之前对应挂起函数所在的线程,而不会强制指定任何特别协程的策略
 * 来自一个事件循环的嵌套协程使用这种分发者可以避免堆栈溢出
 *
 * ### Event loop
 * Event loop semantics is a purely internal concept and have no guarantees on the order of execution
 * except that all queued coroutines will be executed on the current thread in the lexical scope of the outermost
 * unconfined coroutine.
 * 事件循环只是一个单纯的内部概念,它不保证执行的属性,只确定协程队列中的协程会在当前线程执行
 *
 *
 * For example, the following code:
 * ```
 * withContext(Dispatchers.Unconfined) {
 *    println(1)
 *    withContext(Dispatchers.Unconfined) { // Nested unconfined
 *        println(2)
 *    }
 *    println(3)
 * }
 * println("Done")
 * ```
 * 这个程序可能输出123 或132 但是Done一定是最后打印
 * Can print both "1 2 3" and "1 3 2", this is an implementation detail that can be changed.
 * But it is guaranteed that "Done" will be printed only when both `withContext` are completed.
 *
 *
 * Note that if you need your coroutine to be confined to a particular thread or a thread-pool after resumption,
 * but still want to execute it in the current call-frame until its first suspension, then you can use
 * an optional [CoroutineStart] parameter in coroutine builders like
 * [launch][CoroutineScope.launch] and [async][CoroutineScope.async] setting it to the
 * the value of [CoroutineStart.UNDISPATCHED].
 *
 */

fun main()  = runBlocking<Unit> {
    launch (Dispatchers.Unconfined){
        println("Dispatchers.Unconfined, thread:${Thread.currentThread().name}")
        delay(300)
        println("Dispatchers.Unconfined, thread:${Thread.currentThread().name}")
    }

    launch {
        println("No params, thread:${Thread.currentThread().name}")
        delay(2000)
        println("No params, thread:${Thread.currentThread().name}")
    }

}

/**
 * 输出
 * Dispatchers.Unconfined, thread:main
 * No params, thread:main
 * Dispatchers.Unconfined, thread:kotlinx.coroutines.DefaultExecutor
 * No params, thread:main
 *
 * DefaultExecutor它是一个object 并且是一个runnable 本身与时间循环相关
 * internal actual object DefaultExecutor : EventLoopImplBase(), Runnable
 *
 */

class HelloKotlin2 {
}

3 协程调试

import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking

/**
 * 协程调试
 * 在log中显示协程的名称
 * 在Run/Debug configuration中选择想要打印log的文件
 * 选择edit configuration
 * 在VM option上填入 -Dkotlinx.coroutines.debug
 * 这样可以将协程的名字 自动添加到线程后面
 */

// 打印当前线程名称的log函数
private fun log(logMessage: String) = println("[${Thread.currentThread().name}] $logMessage")

fun main() = runBlocking {
    val a = async {
        log("hello")
        10
    }

    val b = async {
        log("welcome")
        20
    }
    log("The result is ${a.await() + b.await()}")
}

/**
 * 输出如下
[main @coroutine#2] hello
[main @coroutine#3] welcome
[main @coroutine#1] The result is 30
分别由主线程的协程2 协程3和协程1执行上述语句
 一个线程执行了3个协程
 */

class HelloKotlin3 {
}

4 协程上下文切换

import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext

/**
 * 协程上下文切换
 *
 * 可以使用runBlocking等协程构建器 或者 withContext切换协程上下文
 */
// 打印当前线程名称的log函数
private fun log(logMessage: String) = println("[${Thread.currentThread().name}] $logMessage")

fun main() {
    // newSingleThreadContext是一个协程分发器
    // Context1 代表上下文的名称 使用use是为了可以自动关闭线程资源
    // use接受一个lambda表达式作为参数 block: (T) -> R
    // 示例中ctx1表示上下文参数
    newSingleThreadContext("Context1").use { ctx1 ->
        log("111")//主线程执行
        newSingleThreadContext("Context2").use { ctx2 ->
            log("222")//主线程执行
            runBlocking(ctx1) {//用runBlocking切换到Context1
                log("started in context1")// 切换到Context1线程的协程1执行这句话
                withContext(ctx2){// 让代码块在ctx2上下文执行
                    log("working in context2")//切换到Context2线程的协程1执行这句话
                }
                log("back to context1")//再次回到Context1线程的协程1执行这句话
            }
        }
    }

}
/**
 * use函数的官方介绍
 * Executes the given [block] function on this resource and then closes it down correctly whether an exception
 * is thrown or not.
 * 在它自己的资源上执行给定block块 并且不管是否发生异常都会正确的关闭资源(不需要调用close方法)
 *
 * @param block a function to process this [Closeable] resource.
 * @return the result of [block] function invoked on this resource.
 */

/**
 * 输出
[main] 111
[main] 222
[Context1 @coroutine#1] started in context1
[Context2 @coroutine#1] working in context2
[Context1 @coroutine#1] back to context1

Context1的协程一号 执行started in context1
Context2的协程一号 执行working in context2
Context1的协程一号 执行back to context1
 两个线程的协程名字一样 他们也不是同一个协程
 */
class HelloKotlin4 {
}

5 Job详解

import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.CoroutineContext

/**
 * Job详解
 *
 * 1.如何获取Job
 * 2.为什么可以这么用
 *
 * 类似于CoroutineScope.launch的方法 它返回一个Job对象 这个要获取对象比较容易
 * 但是像runBlocking这种不将Job返回的方法 如何获取Job对象呢
 * 协程Job是归属其上下文的一部分,Kotlin为我们提供了一种简洁的方式获取协程自身的Job对象
 * 即 coroutineContext[Job]表达式来访问上下文中的对象
 */

fun main() = runBlocking {
    val job: Job? = coroutineContext[Job]
    println(job)
    // public val CoroutineContext.isActive: Boolean
    //    get() = this[Job]?.isActive == true
    // The coroutineContext.isActive expression is a shortcut for coroutineContext[Job]?.isActive == true. See Job.isActive.
    println(coroutineContext.isActive)
    println(coroutineContext[Job]?.isActive)//等价于上面
}

/**
 * 输出
 * "coroutine#1":BlockingCoroutine{Active}@2d38eb89
 */

/**
 * 1.coroutineContext属性是什么
 * coroutineContext是接口 CoroutineScope的一个成员变量 因此基本所有协程都可以直接访问该变量
 * 以下是官方对于coroutineContext变量的介绍
 *
 * The context of this scope.
 * 当前作用域的上下文
 * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
 * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
 * 上下文被当前作用域封装 用于作为作用域扩展的协程构建器的实现
 * 除了访问 [Job] 实例以进行高级用途外,不建议出于任何其他目的在通常代码中访问此属性。
 *
 * By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
 * 按照惯例,应该包含一个 [Job] 的实例来强制结构化并发。
 *
 *
 *
 * 2.coroutineContext属性详解:
 * coroutineContext在AbstractCoroutine有一个实现
 * protected val parentContext: CoroutineContext
 * public final override val context: CoroutineContext = parentContext + this
 * // The context of this scope which is the same as the [context] of this coroutine.
 * public override val coroutineContext: CoroutineContext get() = context
 * 而 runBlocking间接实现了CoroutineScope 因此在runBlocking中可以直接使用coroutineContext对象
 *
 * 3.CoroutineContext
 * Persistent context for the coroutine. It is an indexed set of [Element] instances.
 * An indexed set is a mix between a set and a map.
 * Every element in this set has a unique [Key].
 * CoroutineContext是针对于协程的持久性上下文 它是一种Element实例的带有索引的集合
 * 一个index固定的,介于集合和Map之间的混合体
 * 在这set中 每一个元素含有一个独立的Key
 * 其中有一个属性
 * public operator fun <E : Element> get(key: Key<E>): E?
 * 其作用是Returns the element with the given [key] from this context or `null`.
 * 从当前context返回指定key的元素或者null
 *
 * 4.Job来自哪里 为什么可以通过Job来访问Job对象 通过coroutineContext[Job]表达式来访问上下文中的对象
 * Key for [Job] instance in the coroutine context.
 * 使用Job实例索引可以当前协程上下文中找到Job实例
 *
 * 查看CoroutineContext源码 发现如下定义
 * public interface Key<E : Element>?
 * 官方解释为 Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
 * 而Job定义为 public interface Job : CoroutineContext.Element 他是一个接口 实现了CoroutineContext.Element
 * 他有一个伴生对象public companion object Key : CoroutineContext.Key<Job> 这个伴生对象是协程上下文job实例的key
 * (Key for [Job] instance in the coroutine context.)
 * 因此 Job直接使用 是用了伴生对象的特性
 *
 * 5.输出的BlockingCoroutine是个啥
 * 我们在Builders.kt找到了BlockingCoroutine的定义private class BlockingCoroutine<T>(
 * parentContext: CoroutineContext,
 * private val blockedThread: Thread,
 * private val eventLoop: EventLoop?
 * ) : AbstractCoroutine<T>(parentContext, true)
 *
 * 它的实例化在runBlocking的执行体中
 * val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
 * 可以看到runBlocking的协程最终由BlockingCoroutine执行
 */

class HelloKotlin5 {
}

6 父子协程的关系

import kotlinx.coroutines.*

/**
 * 父子协程的关系
 * GlobalScope在父子协程中的特例
 *
 * 当一个协程是通过另一个协程的CoroutineScope来启动的,那么该协程会通过CoroutineScope.coroutineContext来继承其上下文信息,
 * 同时,新协程的Job就会成为父协程的Job的一个孩子;当父协程被取消执行时,该父协程的所有孩子都会通过递归的方式一起取消
 *
 * 特例:GlobalScope启动协程时,对于新启动的协程来说,他是没有父Job的。因此,它不会绑定到其所启动的协程范围上(GlobalScope),因此
 * 这种情况下,新启动的协程可以独立运行
 * GlobalScope的定义为 public object GlobalScope : CoroutineScope 它是一个object 实现了CoroutineScope接口
 *
 *
 * 官方对GlobalScope的解释为
 * A global [CoroutineScope] not bound to any job.
 * GlobalScope是一个全局的没有绑定到任何job的CoroutineScope
 *
 * Global scope is used to launch top-level coroutines which are operating on the whole application lifetime
 * and are not cancelled prematurely.
 * Another use of the global scope is operators running in [Dispatchers.Unconfined], which don't have any job associated with them.
 * GlobalScope被用来启动顶层协程 这些协程在整个应用生命周期运行且不会过早地取消(注意这里过早地被取消是指被其他协程连带取消 如果是用GlobalScope自己的Job取消
 * 还是可以成功取消的)
 * GlobalScope的另一种用途是结合协程分发器Dispatchers.Unconfined使用,这种情况下启动的协程不会有任何与之关联的job
 *
 * Application code usually should use an application-defined [CoroutineScope]. Using
 * [async][CoroutineScope.async] or [launch][CoroutineScope.launch]
 * on the instance of [GlobalScope] is highly discouraged.
 * 应用代码通常应该使用一个应用程序定义的CoroutineScope。在GlobalScope内部使用CoroutineScope.async或者CoroutineScope.launch是极度不建议的
 *
 *
 * CoroutineScope是什么
 * Defines a scope for new coroutines. Every **coroutine builder** (like [launch], [async], etc)
 * is an extension on [CoroutineScope] and inherits its [coroutineContext][CoroutineScope.coroutineContext]
 * to automatically propagate all its elements and cancellation.
 * CoroutineScope 定义了一个新 Coroutine 的执行 Scope。每个 coroutine builder 都是 CoroutineScope 的扩展函数,
 * 并且自动的继承了当前 Scope 的 coroutineContext 和取消操作
 *
 */
private fun log(logMessage: String) = println("[${Thread.currentThread().name}] $logMessage")

fun main() = runBlocking<Unit> {//runBlocking 在主线程开启了一个协程 该协程会阻塞主线程
    log("outer")
    val job = launch {// launch启动一个新的协程
        log("outer.launch")
        GlobalScope.launch {// GlobalScope.launch再次启动一个协程
            log("job1 hello")
            delay(1000)
            log("job1 world")// 这句话会执行 因为虽然父协程取消了 但GlobalScope.launch没有父协程
        }

        launch {// launch再再次启动一个协程
            delay(100)
            log("job2 hello")
            delay(1000)
            log("job2 world")//这句话不会执行 因为父协程取消之后 子协程也被取消了
        }
    }
    // 最终runBlocking内部有一个launch启动的协程 launch启动的协程内部有两个协程 一个是launch启动的一个是GlobalScope.launch启动的

    delay(500)
    job.cancel()// 这里取消的不是最外层的协程 而是launch启动的协程 // 可以思考一下 如果同时注释这里的job.cancel()和delay(1000) 输出会是什么 学完HelloKotlin7再回来看看呢?
    // launch启动的协程内部有两个协程 按照取消父协程的解释 除了GlobalScope.launch启动的协程 所有子协程都会被取消 可以推测最后输出
    delay(1000)// 注意这里如果注释调这个延时 GlobalScope.launch启动的协程第二次输出也会消失 因为整个程序已经结束了
    log("end")
}

/**
 * 输出
[main @coroutine#1] outer
[main @coroutine#2] outer.launch
[DefaultDispatcher-worker-1 @coroutine#3] job1 hello
[main @coroutine#4] job2 hello
[DefaultDispatcher-worker-1 @coroutine#3] job1 world
[main @coroutine#1] end
 */

class HelloKotlin6 {
}
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

/**
 * 对比HelloKotlin6 思考下面会如何输出
 * 注意加上-Dkotlinx.coroutines.debug
 */

private fun log(logMessage: String) = println("[${Thread.currentThread().name}] $logMessage")


fun main() = runBlocking<Unit> {
    log("outer")
    val job1 = GlobalScope.launch {
        log("job1 hello")
        delay(1000)
        log("job1 world")
    }

    val job2 = launch {
        delay(100)
        log("job2 hello")
        delay(1000)
        log("job2 world")
    }
    delay(500)
    job1.cancel()
    job2.cancel()
    delay(1000)
    log("end")
}

/**
输出
[main @coroutine#1] outer
[DefaultDispatcher-worker-1 @coroutine#2] job1 hello
[main @coroutine#3] job2 hello
[main @coroutine#1] end
 */

class HelloKotlin6_1 {
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值