Liuqichun's Blog

Java 协程

协程本质上和线程是一类概念,是编程过程中对并发计算任务的一种抽象,只不过协程在调度层面上更轻量。

线程的调度由于是操作系统实施的,有时间片/中断等较为复杂的机制,因此调度点对用户是透明的,可以认为调度理论上可以在任何地方触发。而协程的调度点往往由用户代码显式触发(发生在用户态),需要用户代码自己相互“协作”来完成任务的调度和执行,这也是协程中「协」的来源。

一、Java 线程 & 协程

在 Java 中,每个线程都会映射到一个内核级线程,线程的调度是由操作系统内核负责的;对于协程来说,协程的切换仅仅是应用层的切换,不会深入内核。

Java 线程、协程与OS线程映射关系

二、协程的实现机制

从上下文切换方面,协程可以分为有栈协程和无栈协程。

从调度方面,协程可以分为对称协程和非对称协程。

三、协程有哪些优势

3.1 降低损耗

协程无论在创建、销毁、调度等方面,资源开销都远小于线程,因为协程的操作不涉及操作系统的资源分配和调度。

内存占用

协程通常占用更少的内存,因为协程的栈大小比线程小得多。线程在现代操作系统中通常是固定大小(如 1MB),而协程可以小到几 KB(Java G1 512KB)。这使得在同样的条件下,协程能以更高的密度运行。

上下文切换

线程的上下文切换需要系统调用陷入内核。

上下文切换(context-switch)分为两类,一类是主动切换(nr_voluntary_switches),典型的场景是锁、信号量、IO、Thread.yield();另一类是被动切换,典型场景是线程一直在 CPU 计算(busy loop),当一个时间片被用完时发生上下文切换。

线程上下文切换的步骤:

1、保存当前线程的上下文

2、选择新的线程

3、加载新线程的上下文

4、更新内存和缓存状态

5、恢复执行

线程上下文切换消耗组成:

  1. 操作系统保存和恢复上下文所需的开销,这部分开销算在进程本身消耗的 CPU 里
    • 单次上下文切换开销主要受寄存器状态,调度策略,硬件性能等因素影响
    • 上下文切换次数主要受 I/O 操作,锁和同步机制,系统负载,线程的创建和销毁频率等因素影响。
  2. 线程调度器进行线程调度的开销,这部分消耗算在操作系统消耗的 CPU 里

线程上下文切换

协程上下文切换过程:

1、保存当前协程状态

2、选择下一个协程

3、恢复目标协程状态

4、继续执行目标协程

协程的单次上下文切换不需要内核线程调度器参与,仅需要进行少量的软件上下文保存及一些简单的内存管理操作。

协程上下文切换

3.2 提升并发度

突破 thread-per-request 的限制。

对于 Java 线程模型来讲,一个请求每次只能用一个独立的线程处理,且线程对请求的处理是串行的,一个线程同时只能处理一个请求,如果请求进行一些系统调用,那么线程就会被阻塞。对于协程而言,多个协程共用一个线程,且可以线程内部自主切换,从而实现一个线程处理多个请求。

线程、协程处理请求

衡量一个系统的效率通常几个重要参数:QPS(TPS)、并发数、响应时间。

理解了上面三个要素的意义之后,就能推算出它们之间的关系:

通过公式可以得到提升吞吐量,要么提高并发量(度),要么降低响应时间。

3.3 局限性

四、协程的收益

N4dA3D

上下文切换开销,单次协程上下文切换耗时约 0.28us (测试数据 0.3us - 6us),约占线程消耗的 3.7%

收益计算,协程的收益主要体现在 CPU 利用率的后半段,可以帮助我们把 CPU 利用率达到一个更高的水位,同时保持同样的稳定性。

协程收益 = 接入前(原利用率下)CPU 核数 - 接入后(现利用率下)CPU 核数

参考

  1. https://openjdk.org/jeps/425
  2. https://waylau.com/jep-425-virtual-threads-preview/
  3. https://zhuanlan.zhihu.com/p/446993465
  4. https://zhuanlan.zhihu.com/p/535658398
  5. https://github.com/dragonwell-project/dragonwell8/wiki/Wisp%E6%96%87%E6%A1%A3
  6. https://developer.aliyun.com/article/738762
  7. Little 定律

<< Previous Post

|

Next Post >>

#Java