Kotlin学习笔记27 协程part7 父协程总是等待子协程执行完成 给协程取名字 协程操作符重载 让普通类也具有协程特点 协程线程与ThreadLocal

参考链接

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

1  父协程总是等待子协程执行完成

**
 * 父协程总是等待子协程执行完成
 * 对于父协程来说,父协程总是会等待所有子协程完成,而不必显示地追踪由它启动的子协程,子协程也不需要调用自身的Job.join方法来让父协程等待子协程完成
 */
fun main() = runBlocking {
    val job = launch {
        repeat(5) { i ->
            launch {
                delay(i * 200L)
                println("Coroutine $i 执行完毕")
            }
        }
        println("hello")
    }
    /**
     * join方法Suspends the coroutine until this job is complete
     * 这里调用这个方法主要目的是让”Coroutine x 执行完毕“在”end“之前输出
     * 事实上 这里注释调join方法 子协程仍然会能够执行结束
     */
    job.join()
    println("end")
}

class HelloKotlin7 {
}

2 给协程取名字

import kotlinx.coroutines.*

/**
 * 给协程取名字
 *
 * CoroutineName上下文元素可以让我们对协程取名字,以便能够输出可读性较好的日志
 *
 * 注意加上-Dkotlinx.coroutines.debug
 */
private fun log(logMessage: String) = println("[${Thread.currentThread().name}] $logMessage")

/**
 * CoroutineName看其定义:public data class CoroutineName
 * CoroutineName是一个数据类 继承了AbstractCoroutineContextElement AbstractCoroutineContextElement又实现了接口 Element
 * 接口Element则实现了CoroutineContext接口
 * 因此CoroutineName也是一个CoroutineCoroutineContext
 *
 * CoroutineName可以用来给用户指定协程的名称 该名称可以再debug模式使用
 * User-specified name of coroutine. This name is used in debugging mode.
 * See [newCoroutineContext][CoroutineScope.newCoroutineContext] for the description of coroutine debugging facilities.
 */
fun main() = runBlocking(CoroutineName("myCoroutineName1")) {
    log("hello")
    val value1 = async(CoroutineName("myCoroutineName2")) {
        delay(800)
        log("myCoroutine2 log")
        30
    }

    val value2 = async(CoroutineName("myCoroutineName3")) {
        delay(1000)
        log("myCoroutine3 log")
        5
    }
    log("result is ${value1.await() + value2.await()}")// 可以通过在一个deferred上调用await方法来获取最终计算的结果值
}

/**
 输出
 [main @myCoroutineName1#1] hello
 [main @myCoroutineName2#2] myCoroutine2 log
 [main @myCoroutineName3#3] myCoroutine3 log
 [main @myCoroutineName1#1] result is 35
 */

class HelloKotlin8 {
}

3 协程操作符重载

import kotlinx.coroutines.*

/**
 * 既想给协程构建器指定协程名称 又想指定协程分发器
 * 操作符重载
 * 注意加上-Dkotlinx.coroutines.debug
 */
private fun log(logMessage: String) = println("[${Thread.currentThread().name}] $logMessage")

/**
 * 追踪+号 我们发现运算符重载
 * Returns a context containing elements from this context and elements from  other [context].
 * The elements from this context with the same key as in the other one are dropped.
 * ”+“返回了一个一个上下文 该上下文包含了 当前context的元素以及其他context的元素
 * 如果当前context的元素和其他context的元素拥有相同的key 则会被抛弃
 */
fun main() = runBlocking {
    launch(CoroutineName("myCoroutineName") + Dispatchers.Default) {
        log("myCoroutine log")
    }
}

/**
输出
[DefaultDispatcher-worker-1 @myCoroutineName#2] myCoroutine log
 */

class HelloKotlin9 {
}

4 让普通类也具有协程特点

import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.Default

/**
 * 让普通类也具有协程特点
 * 将Activity变成协程 这样 在他里面创建的协程都会变成子协程
 * 模拟Android中Activity关闭 所有子协程都关闭
 * 达到协程的生命周期与Activity的生命周期绑定的效果
 * 写法是让Activity继承CoroutineScope 并在Activity销毁时取消协程 这样 其子协程也会一并取消
 *
 * 在Android开发中 如果Activity退出 而依附于Activity的协程却没有取消 会导致内存泄漏
 * 这里我们模拟创建Activity及其销毁
 */

class Activity:CoroutineScope by CoroutineScope(Dispatchers.Default) {
    fun destroy(){
        cancel()//调用代理类的方法
    }

    fun doSthCostTime(){
        repeat(8){ i->
            launch {
                delay(i*300L)
                println("coroutine $i is finished")
            }
        }
    }
}

fun main()  = runBlocking {
    val activity = Activity()
    println("start coroutine")
    activity.doSthCostTime()
    delay(1300L)

    println("mock activity destroyed")
    activity.destroy()

    delay(3000L)
    println("end")

}

/**
 * CoroutineScope内部实现
 * 官方对于CoroutineScope方法的定义和解释
 * public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
 * ContextScope(if (context[Job] != null) context else context + Job())
 *
 * Creates a [CoroutineScope] that wraps the given coroutine [context].
 * 创建一个包含给定协程上下文的CoroutineScope
 *
 * If the given [context] does not contain a [Job] element, then a default `Job()` is created.
 * This way, cancellation or failure of any child coroutine in this scope cancels all the other children,
 * just like inside [coroutineScope] block.
 * 如果给定的context没有Job元素,那么会创建一个默认的Job,这样,在该范围的任何子协程被取消或出现异常会取消其他子协程,就像其他子协程也在该作用域中
 *
 *
 *
 *
 *
 *
 * CoroutineScope另外一种实现 适用于Android UI线程 即MainScope
 * 该方法专门针对图形界面设计
 * Creates the main [CoroutineScope] for UI components.
 *
 * Example of use:
 * ```
 * class MyAndroidActivity {
 *     private val scope = MainScope()
 *
 *     override fun onDestroy() {
 *         super.onDestroy()
 *         scope.cancel()
 *     }
 * }
 * ```
 *
 * The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements.
 * If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator:
 * `val scope = MainScope() + CoroutineName("MyActivity")`.
 * public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
 * 该CoroutineScope包含了SupervisorJob以及Dispatchers.Main上下文元素
 *
 *
 *
 * 关于Dispatchers.Main
 * A coroutine dispatcher that is confined to the Main thread operating with UI objects.
 * This dispatcher can be used either directly or via [MainScope] factory.
 * Usually such dispatcher is single-threaded.
 * Dispatchers.Main是一个绑定到操作UI对象的主线程的协程分发器
 * 该分发器可以直接使用或者通过MainScope工厂使用
 * 通常 该分发器是单线程的
 *
 * Access to this property may throw [IllegalStateException] if no main thread dispatchers are present in the classpath.
 *
 * Depending on platform and classpath it can be mapped to different dispatchers:
 * - On JS and Native it is equivalent of [Default] dispatcher.
 * - On JVM it is either Android main thread dispatcher, JavaFx or Swing EDT dispatcher. It is chosen by
 *   [`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html).
 *
 * In order to work with `Main` dispatcher, the following artifacts should be added to project runtime dependencies:
 *  - `kotlinx-coroutines-android` for Android Main thread dispatcher
 *  - `kotlinx-coroutines-javafx` for JavaFx Application thread dispatcher
 *  - `kotlinx-coroutines-swing` for Swing EDT dispatcher
 *
 * In order to set a custom `Main` dispatcher for testing purposes, add the `kotlinx-coroutines-test` artifact to
 * project test dependencies.
 *
 * Implementation note: [MainCoroutineDispatcher.immediate] is not supported on Native and JS platforms.
 *
 * public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
 *
 *
 * 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 和取消操作
 */

class HelloKotlin10 {
}

上面只是模拟Android中的Activity 如果实际在Activity中使用 应该如下

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import kotlinx.coroutines.*
// 使用MainScope需要添加依赖kotlinx-coroutines-android以及kotlinx-coroutines-core
// 否则报错 Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher,
// e.g. 'kotlinx-coroutines-android' and ensure it has the same version as 'kotlinx-coroutines-core'
class MainActivity2 : AppCompatActivity(), CoroutineScope by MainScope() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)
        doSthCostTime()
    }

    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }

    fun close(view: View?) {
        this.finish()
    }

    private fun doSthCostTime() {
        repeat(100) { i ->
            launch {
                delay(i * 300L)
                println("coroutine $i is finished")
            }
        }
    }

    fun to3(view: View?) {
        val intent = Intent(this, MainActivity3::class.java)
        startActivity(intent)
    }
}

可以自行搜索MainScope在Android中的应用 不过注意添加额外的gradle依赖

    apply plugin: 'kotlin-android'

    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"

    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"

完整gradle如下

plugins {
    id 'com.android.application'
}
apply plugin: 'kotlin-android'

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        applicationId "com.example.startkotlin"
        minSdkVersion 24
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
}

5  协程线程与ThreadLocal

这里其实主要讲的是ThreadLocal的扩展方法asContextElement

但是我感觉自己还有不少问题 没明白它的真正用途 网上的文章基本copy自Kotlin官网 权且整理我的学习笔记 看看以后能不能解决现在还看不懂的问题吧

import kotlinx.coroutines.*

/**
 * 协程 线程与ThreadLocal
 *
 * 协程不会绑定到特定的线程(比如yield协程后 协程下次继续执行可能会切换到另外的线程
 * 又比如HelloKotlin4中可以切换协程上下文,切换上下文的同时可能就切换了线程)
 * ThreadLocal却是和线程绑定的
 *
 * 本节的目的是协程在A线程运行时 可以得到线程A中保存的数据
 * 如果协程切换到B线程运行 又可以得到线程B中保存的数据
 * 在A B 线程之间来回切换 不会导致数据丢失 错乱
 *
 * 关于ThreadLocal可以参考我之前的文章
 * https://blog.csdn.net/u011109881/article/details/119334195
 * 需要注意其set get方法 以及其中的Map对象的Key 与 Value
 *
 * 最终会发现本案例没有什么特殊的 就是不同的线程中的ThreadLocal里面保存的值不同
 *
 * 可以加上配置 -Dkotlinx.coroutines.debug
 */
private val threadLocal = ThreadLocal()
private fun log(logMessage: String) = println("[${Thread.currentThread().name}] $logMessage")

fun main() = runBlocking {
    threadLocal.set("main")
    log("current thread local value ${threadLocal.get()}")

    val job = launch (Dispatchers.Default+ threadLocal.asContextElement(value = "launch")){
        log("launch current thread local value ${threadLocal.get()}")
        yield()//将当前方法执行者让出来 让其他线程运行
        log("after yield current thread local value ${threadLocal.get()}")
    }
    job.join()
    log("last current thread local value ${threadLocal.get()}")
}

/**
 * asContextElement的部分官方文档
 * 定义:
 * public fun  ThreadLocal.asContextElement(value: T = get()): ThreadContextElement = ThreadLocalElement(value, this)
 *
 * Wraps [ThreadLocal] into [ThreadContextElement]. The resulting [ThreadContextElement]
 * maintains the given [value] of the given [ThreadLocal] for coroutine regardless of the actual thread its is resumed on.
 * By default [ThreadLocal.get] is used as a value for the thread-local variable, but it can be overridden with [value] parameter.
 * Beware that context element **does not track** modifications of the thread-local and accessing thread-local from coroutine
 * without the corresponding context element returns **undefined** value. See the examples for a detailed description.
 *
 * asContextElement是ThreadLocal的扩展方法 它将ThreadLocal封装为ThreadContextElement,ThreadContextElement为协程中给定的ThreadLocal维护给定
 * 的value 而无论该协程运行在那个线程。
 * hreadLocal.get用作thread-local变量的默认值,但是它可以被参数value覆盖
 * 注意上下文元素不会追踪thread-local的修改 并且从不对应的上下文元素访问协程的thread-local会返回不确定的值 参见示例
 *
 *
 * Yield挂起方法:
 * public suspend fun yield(): Unit
 *
 * Yields the thread (or thread pool) of the current coroutine dispatcher to other coroutines to run if possible.
 * yield代表让步 大致意思是 如果可能,让当前协程分发者或其他协程的other线程(或线程池)接着运行下面的代码
 * This suspending function is cancellable.
 * 该挂起方法可以被取消
 * If the [Job] of the current coroutine is cancelled or completed when this suspending function is invoked or while
 * this function is waiting for dispatch, it resumes with a [CancellationException].
 * 如果当挂起方法被调用或该方法正在等待分发的时候,当前协程的Job被取消或者完成,最终会引发 CancellationException异常
 *
 * **Note**: This function always [checks for cancellation][ensureActive] even when it does not suspend.
 *
 *
 * 输出
 * [main @coroutine#1] current thread local value main
 * [DefaultDispatcher-worker-1 @coroutine#2] launch current thread local value launch
 * [DefaultDispatcher-worker-1 @coroutine#2] after yield current thread local value launch
 * [main @coroutine#1] last current thread local value main
 *
 * 我这里的输出与视频不一样 yield之后没有切换线程
 */

class HelloKotlin11 {
}
import kotlinx.coroutines.*

/**
 * 猜想一下这里的代码的输出
 */
private val threadLocal = ThreadLocal()
private fun log(logMessage: String) = println("[${Thread.currentThread().name}] $logMessage")

fun main() = runBlocking {
    threadLocal.set("main")
    log("current thread local value ${threadLocal.get()}")

    val job = launch (Dispatchers.Default){
        threadLocal.set("AA")
        log("current thread local value ${threadLocal.get()}")
        yield()
        log("current thread local value ${threadLocal.get()}")
    }
    job.join()
    log("current thread local value ${threadLocal.get()}")
}

/**
 * 输出
 *
 * [main] current thread local value main
 * [DefaultDispatcher-worker-1] current thread local value AA
 * [DefaultDispatcher-worker-1] current thread local value AA
 * [main] current thread local value main
 *
 * 不明白threadLocal.asContextElement的意义了 不指定这个 线程里面的数据还是可以在协程间切换
 * 那么asContextElement的意义何在
 */

class HelloKotlin11_1 {
}

asContextElement示例

/**
 * asContextElement示例
 */

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

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

fun main() = runBlocking {
    val myThreadLocal = ThreadLocal()
    val threadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
    // 下面的这句运行在主线程的协程1 myThreadLocal没有初始化 得到null
    log("0" + myThreadLocal.get()) // Prints "null"
    launch(Dispatchers.Default + myThreadLocal.asContextElement(value = "foo")) {
        // 下面的这句切换线程 运行在DefaultDispatcher-worker-2线程 myThreadLocal刚初始化为foo 得到foo
        log("1" + myThreadLocal.get()) // Prints "foo"
        withContext(threadDispatcher) {
            // 下面的这句再次切换线程池 运行在pool-1-thread-1 @coroutine#2 myThreadLocal的值延用外层的值 得到foo
            log("2" + myThreadLocal.get()) // Prints "foo", but it's on other thread
        }
    }
    // 下面的这句运行在主线程的协程1 主线程的myThreadLocal仍然没有初始化 得到null
    log("3" + myThreadLocal.get()) // Prints "null"


    //The context element does not track modifications of the thread-local variable, for example:
    //上下文元素不会追踪thread-local值的修改


    myThreadLocal.set("main")//修改主线程myThreadLocal的值

    // 下面的这句再次切换线程池 运行在pool-1-thread-1 @coroutine#1
    withContext(threadDispatcher) {
        log("4" + myThreadLocal.get()) // Prints "main" // 与官网说的不一样 这里输出null???
        myThreadLocal.set("UI")
    }
    // 下面的这句运行在主线程的协程1 主线程的myThreadLocal被初始化为main 得到main 与其他线程的值修改无关
    log("5" + myThreadLocal.get()) // Prints "main", not "UI"


    //Use `withContext` to update the corresponding thread-local variable to a different value, for example:
    // 使用withContext更新对应线程的thread-local变量为不同的值

    withContext(myThreadLocal.asContextElement(value = "foo")) {// 将主线程中的thread-local的值修改为foo
        // 下面的这句运行在主线程的协程1 得到值为foo
        log("6" + myThreadLocal.get()) // Prints "foo"
    }


    //Accessing the thread-local without corresponding context element leads to undefined value:
    // 访问不对应上下文元素的thread-local 会导致得到一个不确定的值 这里还有问题

    val tl = ThreadLocal.withInitial { "initial" }// 初始化ThreadLocal的初始值为initial

    runBlocking {
        // 下面的这句运行在主线程的协程3 "main @coroutine#3" 得到值为initial
        log("7" + tl.get()) // Will print "initial"
        // Change context
        withContext(tl.asContextElement("modified")) {// 使用withContext更新”main @coroutine#3“的ThreadLocal值为modified
            // // 下面的这句运行在主线程的协程3 "main @coroutine#3" 得到值为modified
            log("8" + tl.get()) // Will print "modified"
        }
        // Context is changed again

        log("9" + tl.get()) // <- WARN: can print either "modified" or "initial" // 这里我的输出一直都是initial???

    }
    threadDispatcher.close()
}

/*
我的输出
[main @coroutine#1] 0null
[main @coroutine#1] 3null
[DefaultDispatcher-worker-1 @coroutine#2] 1foo
[pool-1-thread-1 @coroutine#2] 2foo
[pool-1-thread-1 @coroutine#1] 4null
[main @coroutine#1] 5main
[main @coroutine#1] 6foo
[main @coroutine#3] 7initial
[main @coroutine#3] 8modified
[main @coroutine#3] 9initial

Process finished with exit code 0
 */

class HelloKotlin11_2 {
}

你可能感兴趣的:(Kotlin,kotlin,开发语言,android)