Kotlin协程学习

 

协程的介绍

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed. Coroutines are well-suited for implementing familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes.

According to Donald Knuth, Melvin Conway coined the term coroutine in 1958 when he applied it to the construction of an assembly program.[1] The first published explanation of the coroutine appeared later, in 1963.[2]

协程和thread类似,但是和thread也有很大的区别

协程的理解

协程也可以叫做微线程,是一种新的多任务并发的操作手段

  • 是一个运行在单线程中的并发程序
  • 省去了传统thread多线程并发机制中切换线程带来的线程上下文切换线程状态切换thread初始化的性能消耗,可以大幅度提高并发性能
  • 简而言之,协程是在单线程上由程序员自己调度运行的并行计算机制

协程是一个非常轻量级的线程,线程是系统级的协程是编译器级的

Coroutine的实现通常是对某个语言做出相应的提议,通过后作为编译器标准,之后编译器厂商来实现该机制,thread是操作系统的机制,通过API暴露给下层调用

协程和线程的对比

Thread

  • 拥有独立的栈,局部变量,基于进程的内存共享,数据共享较为容易,在多线程开发时需要使用锁来进行访问,不然会导致数据错误,锁多会死锁。
  • 线程之间的调度由内核来控制,开发者无法介入,线程的切换深度到内核级别
  • 线程切换的代价较大
    • 线程对象的创建和初始化
    • 线程上下文切换
    • 线程状态的切换由系统内核完成
    • 对变量的操作要加锁

Coroutine

  • 是线程上的优化产物,轻量级的Thread,有自己的栈内存和局部变量,共享成员变量
  • 传统的Thread执行的核心是一个while(true)函数来进行暴力循环,是一个耗时函数,Coroutine可以用来直接标记方法,程序员自己实现切换和调度
  • 一个Thread上可以同时跑多个Coroutine,同一时间只有一个Coroutine被执行,在单线程上模拟多线程的并发
  • 因为在同一个线程中,Coroutine之间的切换不涉及到线程的上下文切换和线程状态的改变,不存在资源和数据的并发,不需要加锁,只需要判断状态就可以
  • Coroutine是非阻塞式的,一个协程在进入阻塞后不会阻塞当前的线程,当前线程会去执行其他的协程任务

开发者通过yieldresume两个API来完成Coroutine操作

  • yield可以让协程在空闲(例如等待IO或者网络请求)的时候放弃执行权
  • resume可以唤醒协程继续运行
  • 协程一旦开始运行就不会结束,直到运行yield后交出执行权

使用这一对API可以非常方便地实现异步运行

Kotlin的协程使用

Kotlin协程在CoroutineScope的上下文中通过launch,async等协程构造器来声明并启动

首先需要导入依赖

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

简单demo

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    doCoroutineOperation()
}

private fun doCoroutineOperation() {
    GlobalScope.launch(Dispatchers.IO) {
        delay(1000)
        printLog("Launch")
    }
    Thread.sleep(2000)
    printLog("End")
}

private fun printLog(log: String) {
    Log.i(TAG, "${Thread.currentThread().name}:$log")
}

运行结果

2022-10-05 17:14:00.951 4431-4464/org.phcbest.kotlincoroutine I/MainActivity: DefaultDispatcher-worker-1:Launch
2022-10-05 17:14:02.949 4431-4431/org.phcbest.kotlincoroutine I/MainActivity: main:End

在这个demo中,使用全局作用域启动了一个协程,延迟一秒后输出了日志,输出的结果显示,协程是运行在协程内部的线程池

从表现结果来看,启动一个协程类似于直接使用Thread来执行耗时任务,但实际上协程和线程有本质上的区别,通过协程可以极大提高线程的并发效率,避免嵌套回调地狱

在这个简单的demo中涉及到了协程的四个基础概念

  • suspend function 挂起函数 delay就是携程库提供的一个非阻塞式延迟的挂起函数
  • CoroutineScope 协程作用域 GlobalScope就是CoroutineScope的一个实现类,用来指定协程的作用范围,可以用来管理多个协程的生命周期,所有的协程都需要通过CoroutineScope来启动
  • CoroutineContext 协程上下文 包含多种类型的配置参数,调用GlobalScope.launch的时候传递的Dispatchers.IO就是CoroutineCoontext这个抽象概念实现,用来指定协程的运行载体,指定协程要运行在哪一类线程上
  • CoroutineBuilder 协程构建器 demo中使用GlobalScope.launch来构建协程,launch和async都是协程的构建器

Kotlin协程的分类

  • launch 创建协程
  • async 创建带返回值的协程,返回Deferred类
  • withContext 不创建新的协程,在指定的协程上运行代码
  • runBlocking 不是GlobalScope的API,可以独立使用,但是runBlocking里面的delay会阻塞线程,而launch创建的不会阻塞