• 概念与实现
  • 协程的使用
  • vs线程进程
  • 协程的优点
  • kotlin的协程

(2019.09.20在团队的分享)


概念与实现

  • 运行在单线程中的并发代码段,或:
  • 一种用户态的轻量级线程

bonus:

  • 并发 vs 并行
  • 用户态 vs 内核态

概念与实现

  • 属性: 拥有自己的寄存器上下文
  • 行为: 切换时,保存当前状态或恢复之前状态

因此:

  • 可不必与特定的线程绑定,可以在一个线程中暂停,并在另一个线程中恢复

bonus: 对比尾递归


概念与实现

  • 有栈协程(Stackful):有自己的调用栈
  • Golang,栈内存可以根据需要进行扩容和缩容,最小一般为内存页长 4KB。
  • 无栈协程(Stackless):没有自己的调用栈
  • PythonKotlin。上下文通过CPS(continuation-passing-style)保存,在Kotlin中,就是一个Continuation类,可想像成Callback

bonus: CPS vs Direct Style


协程的使用

python

def consumer(name):
    while True:
        bone = yield  # 接收producer的传参,执行后面代码,直到再次碰到yield后返回producer
        print("\033[31;1m[consumer] %s\033[0m 消费 %s " % (name, bone))

def producer(obj1, obj2):
    obj1.send(None) or obj2.send(None)   # 发None启动消费者
    for n in range(5):
        print("\033[32;1m[producer]\033[0m 生产 %s" % n)
        obj1.send(n) or obj2.send(n)    # 暂停producer,并切换到consumer
        time.sleep(1)

bonus: 对比子程序


协程的使用

单步调试,注意send(1)之后跳到了yield(2):


协程的使用

kotlin版:playgroud

val channel = Channel<Int>()

GlobalScope.launch {
  for (x in 1..5) {
    println("[producer] 生产 $x")
    channel.send(x)
  }
}

GlobalScope.launch {
  while (true) {
    println("[consumer] 消费 ${channel.receive()}")
  }
}

bonus: 为何pythonkotlin版输出不同?


协程的使用


协程的使用

子程序就是协程的一种特例。 —— Donald Knuth

或者说,协程是子程序的泛化。

为什么?


vs 线程、进程

  • 进程 应用程序的启动实例,有代码和打开的文件资源、数据资源、独立的内存空间。最小的资源管理单元。
  • 线程 从属于进程,有自己的栈空间。最小的执行单元。

表面看它们是语言特性,本质却是操作系统能力,通过API暴露给用户使用。


vs 线程、进程


vs 线程、进程


vs 线程、进程

  • 谁来调度
  • 何时切换

线程/进程是os通过调度算法,保存当前的上下文实现暂停,重新开始的地方不可预期。每次CPU计算的指令数量和代码跑过的CPU时间有关,跑到os分配的cpu时间到达后就会被os强制挂起。

Coroutine是编译器的魔术,通过插入相关的代码使得代码段能够实现分段式的执行,重新开始的地方是yield关键字指定的,一次一定会运行到yield语句,所以本质是程序员决定何时挂起。


协程的优点

  // 可以轻松执行以下代码:
  for (i in 1..1000_000) {
    GlobalScope.launch {
      for (x in 1..300000) {
        println("${Thread.currentThread().name} is busy calculating")
      }
    }
  }

  // 视机器配置,可能无法运行下面代码:
  for (i in 1..1000_000) {
    Thread {
      for (x in 1..300000) {
        println("${Thread.currentThread().name} is busy calculating")
      }
    }.start()
  }

协程的优点

开销小

线程的时间成本可以拆解为:

  • 切换本身的开销,主要是寄存器保存和恢复的成本,可腾挪的余地非常有限;
  • 执行体的调度开销,主要是如何在大量已准备好的执行体中选出谁获得执行权;
  • 执行体之间的同步与互斥成本。

线程的空间成本可以拆解为:

  • 执行体的执行状态
  • TLS(线程局部存储);
  • 执行体的堆栈

显然,上述成本的比重各不相同。

  • 默认情况下Linux 线程在数MB 左右,其中最大的成本是堆栈。如果一个线程 1MB,那么有 1000 个线程就已经到 GB 级别了。
  • 执行体的调度开销,以及执行体之间的同步与互斥成本,也是一个不可忽略的成本。单位成本看起来不大,但扛不住次数太多。

for core2 and modern Linux context switch may cost 5-7 microseconds.

bonus: 每秒多少cs是合理的?

不易出错

  • 共享变量的同步锁

线程的任务分配是抢占式,存在共享变量时,需要使用锁来保证线程间数据安全。 协程间任务分配是分发式,本身无此问题,但如果运行在多线程中,依然有问题

No silver bullet


协程的应用

协程主要应用场景是高性能的网络服务

  • 来自客户端的请求包和服务器的返回包,都是网络IO;
  • 过程中,需要访问存储来保存和读取自身的状态,也涉及本地或网络IO。

如果用多线程来实现,如上所述,成本高,易出错。


Kotlin协程

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

CoroutineContext: 可以理解为协程的上下文,其中一个实现CoroutineDispatcher支持4种线程模式:

  • Dispatchers.Default, 默认线程池, CPU-heavy任务
  • Dispatchers.IO, 适合IO-heavy任务
  • Dispatchers.Main, 主线程
  • Dispatchers.Unconfined, 没指定,就是在当前线程
import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello,") // main thread continues while coroutine is delayed
    Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

参考资料