19讲如何用协程来优化多线程业务 - 图119讲如何⽤协程来优化多线程业务

你好,我是刘超。

近⼀两年,国内很多互联⽹公司开始使⽤或转型Go语⾔,其中⼀个很重要的原因就是Go语⾔优越的性能表现,⽽这个优势与
19讲如何用协程来优化多线程业务 - 图2Go实现的轻量级线程Goroutines(协程Coroutine)不⽆关系。那么Go协程的实现与Java线程的实现有什么区别呢?

线程实现模型

了解协程和线程的区别之前,我们不妨先来了解下底层实现线程⼏种⽅式,为后⾯的学习打个基础。

实现线程主要有三种⽅式:轻量级进程和内核线程⼀对⼀相互映射实现的1:1线程模型、⽤户线程和内核线程实现的N:1线程模型以及⽤户线程和轻量级进程混合实现的N:M线程模型。

1:1线程模型

以上我提到的内核线程(Kernel-Level Thread, KLT)是由操作系统内核⽀持的线程,内核通过调度器对线程进⾏调度,并负责完成线程的切换。

我们知道在Linux操作系统编程中,往往都是通过fork()函数创建⼀个⼦进程来代表⼀个内核中的线程。⼀个进程调⽤fork()函数后,系统会先给新的进程分配资源,例如,存储数据和代码的空间。然后把原来进程的所有值都复制到新的进程中,只有少数值与原来进程的值(⽐如PID)不同,这相当于复制了⼀个主进程。

采⽤fork()创建⼦进程的⽅式来实现并⾏运⾏,会产⽣⼤量冗余数据,即占⽤⼤量内存空间,⼜消耗⼤量CPU时间⽤来初始化内存空间以及复制数据。

如果是⼀份⼀样的数据,为什么不共享主进程的这⼀份数据呢?这时候轻量级进程(Light Weight Process,即LWP)出现了。

相对于fork()系统调⽤创建的线程来说,LWP使⽤clone()系统调⽤创建线程,该函数是将部分⽗进程的资源的数据结构进⾏复制,复制内容可选,且没有被复制的资源可以通过指针共享给⼦进程。因此,轻量级进程的运⾏单元更⼩,运⾏速度更快。

LWP是跟内核线程⼀对⼀映射的,每个LWP都是由⼀个内核线程⽀持。

N:1线程模型

1:1线程模型由于跟内核是⼀对⼀映射,所以在线程创建、切换上都存在⽤户态和内核态的切换,性能开销⽐较⼤。除此之外,它还存在局限性,主要就是指系统的资源有限,不能⽀持创建⼤量的LWP。

N:1线程模型就可以很好地解决1:1线程模型的这两个问题。

该线程模型是在⽤户空间完成了线程的创建、同步、销毁和调度,已经不需要内核的帮助了,也就是说在线程创建、同步、销毁的过程中不会产⽣⽤户态和内核态的空间切换,因此线程的操作⾮常快速且低消耗。

N:M线程模型

N:1线程模型的缺点在于操作系统不能感知⽤户态的线程,因此容易造成某⼀个线程进⾏系统调⽤内核线程时被阻塞,从⽽导致整个进程被阻塞。

N:M线程模型是基于上述两种线程模型实现的⼀种混合线程管理模型,即⽀持⽤户态线程通过LWP与内核线程连接,⽤户态的线程数量和内核态的LWP数量是N:M的映射关系。

了解完这三个线程模型,你就可以清楚地了解到Go协程的实现与Java线程的实现有什么区别了。

JDK 1.8 Thread.java 中 Thread#start ⽅法的实现,实际上是通过Native调⽤start0⽅法实现的;在Linux下, JVM Thread的实现是基于pthread_create实现的,⽽pthread_create实际上是调⽤了clone()完成系统调⽤创建线程的。

所以,⽬前Java在Linux操作系统下采⽤的是⽤户线程加轻量级线程,⼀个⽤户线程映射到⼀个内核线程,即1:1线程模型。由于线程是通过内核调度,从⼀个线程切换到另⼀个线程就涉及到了上下⽂切换。

⽽Go语⾔是使⽤了N:M线程模型实现了⾃⼰的调度器,它在N个内核线程上多路复⽤(或调度)M个协程,协程的上下⽂切换是在⽤户态由协程调度器完成的,因此不需要陷⼊内核,相⽐之下,这个代价就很⼩了。

协程的实现原理

协程不只在Go语⾔中实现了,其实⽬前⼤部分语⾔都实现了⾃⼰的⼀套协程,包括C#、erlang、python、lua、javascript、
ruby等。

相对于协程,你可能对进程和线程更为熟悉。进程⼀般代表⼀个应⽤服务,在⼀个应⽤服务中可以创建多个线程,⽽协程与进程、线程的概念不⼀样,我们可以将协程看作是⼀个类函数或者⼀块函数中的代码,我们可以在⼀个主线程⾥⾯轻松创建多个协程。

程序调⽤协程与调⽤函数不⼀样的是,协程可以通过暂停或者阻塞的⽅式将协程的执⾏挂起,⽽其它协程可以继续执⾏。这⾥的挂起只是在程序中(⽤户态)的挂起,同时将代码执⾏权转让给其它协程使⽤,待获取执⾏权的协程执⾏完成之后,将从挂起点唤醒挂起的协程。 协程的挂起和唤醒是通过⼀个调度器来完成的。

结合下图,你可以更清楚地了解到基于N:M线程模型实现的协程是如何⼯作的。

假设程序中默认创建两个线程为协程使⽤,在主线程中创建协程ABCD…,分别存储在就绪队列中,调度器⾸先会分配⼀个⼯作线程A执⾏协程A,另外⼀个⼯作线程B执⾏协程B,其它创建的协程将会放在队列中进⾏排队等待。

19讲如何用协程来优化多线程业务 - 图3

当协程A调⽤暂停⽅法或被阻塞时,协程A会进⼊到挂起队列,调度器会调⽤等待队列中的其它协程抢占线程A执⾏。当协程A
被唤醒时,它需要重新进⼊到就绪队列中,通过调度器抢占线程,如果抢占成功,就继续执⾏协程A,失败则继续等待抢占线程。

19讲如何用协程来优化多线程业务 - 图4

相⽐线程,协程少了由于同步资源竞争带来的CPU上下⽂切换,I/O密集型的应⽤⽐较适合使⽤,特别是在⽹络请求中,有较
多的时间在等待后端响应,协程可以保证线程不会阻塞在等待⽹络响应中,充分利⽤了多核多线程的能⼒。⽽对于CPU密集型的应⽤,由于在多数情况下CPU都⽐较繁忙,协程的优势就不是特别明显了。

Kilim协程框架

虽然这么多的语⾔都实现了协程,但⽬前Java原⽣语⾔暂时还不⽀持协程。不过你也不⽤泄⽓,我们可以通过协程框架在
Java中使⽤协程。

⽬前Kilim协程框架在Java中应⽤得⽐较多,通过这个框架,开发⼈员就可以低成本地在Java中使⽤协程了。

在Java中引⼊ Kilim ,和我们平时引⼊第三⽅组件不太⼀样,除了引⼊jar包之外,还需要通过Kilim提供的织⼊(Weaver)⼯具对Java代码编译⽣成的字节码进⾏增强处理,⽐如,识别哪些⽅式是可暂停的,对相关的⽅法添加上下⽂处理。通常有以 下四种⽅式可以实现这种织⼊操作:
在编译时使⽤maven插件;
在运⾏时调⽤kilim.tools.Weaver⼯具;
在运⾏时使⽤kilim.tools.Kilim invoking调⽤Kilim的类⽂件;
在main函数添加 if (kilim.tools.Kilim.trampoline(false,args)) return。

Kilim框架包含了四个核⼼组件,分别为:任务载体(Task)、任务上下⽂(Fiber)、任务调度器(Scheduler)以及通信载体
(Mailbox)。
19讲如何用协程来优化多线程业务 - 图5
Task对象主要⽤来执⾏业务逻辑,我们可以把这个⽐作多线程的Thread,与Thread类似,Task中也有⼀个run⽅法,不过在
Task中⽅法名为execute,我们可以将协程⾥⾯要做的业务逻辑操作写在execute⽅法中。

与Thread实现的线程⼀样,Task实现的协程也有状态,包括:Ready、Running、Pausing、Paused以及Done总共五种。
Task对象被创建后,处于Ready状态,在调⽤execute()⽅法后,协程处于Running状态,在运⾏期间,协程可以被暂停,暂停中的状态为Pausing,暂停后的状态为Paused,暂停后的协程可以被再次唤醒。协程正常结束后的状态为Done。

Fiber对象与Java的线程栈类似,主要⽤来维护Task的执⾏堆栈,Fiber是实现N:M线程映射的关键。

Scheduler是Kilim实现协程的核⼼调度器,Scheduler负责分派Task给指定的⼯作者线程WorkerThread执⾏,⼯作者线程
WorkerThread默认初始化个数为机器的CPU个数。

Mailbox对象类似⼀个邮箱,协程之间可以依靠邮箱来进⾏通信和数据共享。协程与线程最⼤的不同就是,线程是通过共享内
存来实现数据共享,⽽协程是使⽤了通信的⽅式来实现了数据共享,主要就是为了避免内存共享数据⽽带来的线程安全问题。

协程与线程的性能⽐较

接下来,我们通过⼀个简单的⽣产者和消费者的案例,来对⽐下协程和线程的性能。可通过 Github 下载本地运⾏代码。

Java多线程实现源码:



public class MyThread {
private static Integer count = 0;//
private static final Integer FULL = 10; //最⼤⽣产数量private static String LOCK = “lock”; //资源锁

public static void main(String[] args) { MyThread test1 = new MyThread();

long start = System.currentTimeMillis();


List list = new ArrayList();
for (int i = 0; i < 1000; i++) {//创建五个⽣产者线程Thread thread = new Thread(test1.new Producer()); thread.start();
list.add(thread);
}

for (int i = 0; i < 1000; i++) {//创建五个消费者线程Thread thread = new Thread(test1.new Consumer()); thread.start();
list.add(thread);
}


try {
for (Thread thread : list) {
thread.join();//等待所有线程执⾏完
}
} catch (InterruptedException e) { e.printStackTrace();
}


long end = System.currentTimeMillis();
System.out.println(“⼦线程执⾏时⻓:” + (end - start));
}
//⽣产者
class Producer implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) { synchronized (LOCK) {
while (count == FULL) {//当数量满了时
try { LOCK.wait();
} catch (Exception e) { e.printStackTrace();
}
}
count++;
System.out.println(Thread.currentThread().getName() + “⽣产者⽣产,⽬前总共有” + count); LOCK.notifyAll();
}
}
}
}
//消费者
class Consumer implements Runnable { public void run() {
for (int i = 0; i < 10; i++) { synchronized (LOCK) {
while (count == 0) {//当数量为零时
try { LOCK.wait();
} catch (Exception e) {
}
}
count—;
System.out.println(Thread.currentThread().getName() + “消费者消费,⽬前总共有” + count); LOCK.notifyAll();
}
}
}
}
}

Kilim协程框架实现源码:

public class Coroutine {
19讲如何用协程来优化多线程业务 - 图6

static Map> mailMap = new HashMap>();//为每个协程创建⼀个信public static void main(String[] args) {

if (kilim.tools.Kilim.trampoline(false,args)) return; Properties propes = new Properties();
propes.setProperty(“kilim.Scheduler.numThreads”, “1”);//设置⼀个线程
System.setProperties(propes);
long startTime = System.currentTimeMillis(); for (int i = 0; i < 1000; i++) {//创建⼀千⽣产者
Mailbox mb = new Mailbox(1, 10); new Producer(i, mb).start();
mailMap.put(i, mb);
}

for (int i = 0; i < 1000; i++) {//创建⼀千个消费者new Consumer(mailMap.get(i)).start();
}

Task.idledown();//开始运⾏

long endTime = System.currentTimeMillis();

System.out.println( Thread.currentThread().getName() + “总计花费时⻓:” + (endTime- startTime));
}

}

19讲如何用协程来优化多线程业务 - 图7
;
//⽣产者
public class Producer extends Task {
Integer count = null;
Mailbox mb = null;
public Producer(Integer count, Mailbox mb) { this.count = count;
this.mb = mb;
}
public void execute() throws Pausable { count = count*10;
for (int i = 0; i < 10; i++) {
mb.put(count);//当空间不⾜时,阻塞协程线程
System.out.println(Thread.currentThread().getName() + “⽣产者⽣产,⽬前总共有” + mb.size() + “⽣产了:” + count) count++;
}
}
}



//消费者
public class Consumer extends Task {


Mailbox mb = null;


public Consumer(Mailbox mb) { this.mb = mb;
}


/*
执 ⾏
*/
public void execute() throws Pausable { Integer c = null;
for (int i = 0; i < 10000; i++) { c = mb.get();//获取消息,阻塞协程线程


if (c == null) {
System.out.println(“计数”);
}else {
System.out.println(Thread.currentThread().getName() + “消费者消费,⽬前总共有” + mb.size() + “消费了:” + c); c = null;
}
}
}
}

在这个案例中,我创建了1000个⽣产者和1000个消费者,每个⽣产者⽣产10个产品,1000个消费者同时消费产品。我们可以
看到两个例⼦运⾏的结果如下:



多线程执⾏时⻓:2761


协程执⾏时⻓:1050

通过上述性能对⽐,我们可以发现:在有严重阻塞的场景下,协程的性能更胜⼀筹。其实,I/O阻塞型场景也就是协程在Java 中的主要应⽤。

总结

协程和线程密切相关,协程可以认为是运⾏在线程上的代码块,协程提供的挂起操作会使协程暂停执⾏,⽽不会导致线程阻塞。

协程⼜是⼀种轻量级资源,即使创建了上千个协程,对于系统来说也不是很⼤的负担,但如果在程序中创建上千个线程,那系

统可真就压⼒⼭⼤了。可以说,协程的设计⽅式极⼤地提⾼了线程的使⽤率。

通过今天的学习,当其他⼈侃侃⽽谈Go语⾔在⽹络编程中的优势时,相信你不会⼀头雾⽔。学习Java的我们也不要觉得,协程离我们很遥远了。协程是⼀种设计思想,不仅仅局限于某⼀⻔语⾔,况且Java已经可以借助协程框架实现协程了。

但话说回来,协程还是在Go语⾔中的应⽤较为成熟,在Java中的协程⽬前还不是很稳定,重点是缺乏⼤型项⽬的验证,可以说Java的协程设计还有很⻓的路要⾛。

思考题

在Java中,除了Kilim框架,你知道还有其它协程框架也可以帮助Java实现协程吗?你使⽤过吗?

期待在留⾔区看到你的⻅解。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他⼀起讨论。
19讲如何用协程来优化多线程业务 - 图8

  1. 精选留⾔ <br />![](https://cdn.nlark.com/yuque/0/2022/png/1852637/1646315871901-96394a64-e092-4445-93a4-a50862ee2c24.png#)周星星<br />⽼师有⼏个疑问
  1. 协程必须⼿动调⽤等待或阻塞才能被安排到等待队列去吗?还是说协程也可以跟线程⼀样会被随机丢到等待队列去每个协程也有个运⾏时间⽚?如果可以随机⼀般是如何实现的?
  2. 协程之间的争抢基于什么实现的?我想的话可以使⽤CAS来实现没有抢到的再次被丢到等待队列不知道对不对。
  3. 我看例⼦上邮箱⾥⾯没有数据时消费者协程没有类似线程的等待机制,这个要如何写呢?

2019-07-02 13:01
19讲如何用协程来优化多线程业务 - 图9QQ怪
⽼师讲协程讲的最深最易懂的⼀个,赞赞赞
2019-07-02 09:19
19讲如何用协程来优化多线程业务 - 图10⾏者
从前,操作系统调度线程运⾏;现在,有了协程,进程⾃⼰调度协程如何运⾏,避免不必要的上下⽂切换,且协程相⽐线程占
⽤内存空间更少。
协程主要来解决⽹络IO问题。
2019-07-19 08:46
19讲如何用协程来优化多线程业务 - 图11宝⽟
有没有具体场景的实战优化呢?
2019-07-06 15:41
作者回复
在socket通信中,可以使⽤协程优化IO读写这块。例如,可以尝试⽤协程写个⾮阻塞Socket通信,或⽤协程简单改造下Netty。具体的实现⾃⼰可以尝试,如果遇到问题欢迎沟通。
2019-07-07 09:47

19讲如何用协程来优化多线程业务 - 图12⼩橙橙
⽼师,以后的JAVA版本是不是也会⾃带协程功能?
2019-07-05 17:28
作者回复
是的,Java未来的三个主要项⽬之⼀Loom项⽬ 引⼊了被称为 fibers 的新型轻量级⽤户线程,甲⻣⽂公司 Loom 项⽬技术负责
⼈ Ron Pressler 在 QCon 伦敦 2019 ⼤会上指出:“利⽤ fibers,如果我们确保其轻量化程度⾼于内核提供的线程,那么问题就得到了解决。⼤家将能够尽可能多地使⽤这些⽤户模式下的轻量级线程,且基本不会出现任何阻塞问题”。

具体的可以阅读以下openjdk官⽹链接:
https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html
2019-07-07 10:13

19讲如何用协程来优化多线程业务 - 图13Liam
消费者从mailbox拿数据为空时,或⽣产者往mailbox写数据没有可⽤空间时,不会阻塞吗?类似于队列
2019-07-03 09:02
作者回复
会的,在mailbox为空时,消费者get会被阻塞;在mailbox满了,⽣产者put会阻塞。
2019-07-04 10:33

19讲如何用协程来优化多线程业务 - 图14cricket1981
AKKA Framework算得上⼀个,另外还有Vert.X
2019-07-02 16:48

19讲如何用协程来优化多线程业务 - 图15-W.LI-
⽼师好!1:1,N:1,N:M的线程模型。总感觉上学的时候有讲可是⼜想不起来。谢谢⽼师的讲解。不过还是有些不明⽩的地⽅。
Java是采⽤1:1的映射⽅式。⼀个Java线程就对应⼀个内核线程。⽤户态和内核态的切换和这个映射模型有关系么(⽤户态和内核态,和⽤户线程内核线程是否有关系)?
从⽤户态切换为内核态的时候具体做了哪些操作?之前讲IO模型时⽼师讲了,⽤户空间和内核空间,多次数据拷⻉。和⽤户线程内核线程有什么联系么?后⾯会讲么?
2019-07-02 13:23
19讲如何用协程来优化多线程业务 - 图16周星星
有个疑问,线程的切换是由系统来保证的,那协程之间的切换也能跟线程那样由调度器保证,还是说协程必须要⼿动调⽤或io
2019-07-02 12:48
作者回复
由调度器保证
2019-07-04 10:29

19讲如何用协程来优化多线程业务 - 图17⻛翱
⼀核的CPU是否有使⽤协程的必要? 按⽬前cpu的调度是采⽤时间⽚的⽅式,⼀核的CPU也存在上下⽂切换。⼀核的CPU也可以应⽤到协程带来的好处。 不知道这个理解是否正确?
2019-07-02 12:11
作者回复
对的
2019-07-04 10:29

19讲如何用协程来优化多线程业务 - 图18crazypokerk
⽼师,不是建议不要使⽤String对象作为锁对象吗?为什么上⾯的代码private static String LOCK = “lock”,要这么写呢?
2019-07-02 09:43
作者回复
没有其他成员变量引⽤”lock”,这⾥使⽤没有问题,具体场景具体分析。
2019-07-04 10:25

19讲如何用协程来优化多线程业务 - 图19Jxin
loom项⽬也可以让javaer玩玩协程,不过仅限于玩玩。总归没有⼤项⽬验证,⽽且都⾮官⽅版本,⽽是以框架的形式引⽤。不建议在真实项⽬中去使⽤。另外erland的并发编程并不弱于go。就⽬前来看,感觉还是java⽐较适合写web项⽬。虽然go写并发编程很爽,但web开发组件不健全,很多东⻄得⾃⼰实现,⽽⾃⼰实现意味着成本和⻛险。能有现有,经过⼤项⽬和时间验证的组件终是⽐较让⼈舒⼼的。
2019-07-02 09:36
作者回复
对的,⼀些技术框架还是需要经受⼤公司或者⼀些⼤型项⽬的验证,不过我们也可以先在⼀些⼩型项⽬中先⾏使⽤。
2019-07-02 11:17

19讲如何用协程来优化多线程业务 - 图20QQ怪
听过quasar,好像这个早就有了,但不知道为啥没⽕起来
2019-07-02 09:21
19讲如何用协程来优化多线程业务 - 图21明翼
除了内存模型还有线程模型,这种有虚拟机的语⾔是不是都有这种对应关系啊。协程⽤的不多,以前想⽤kilim但是有点麻烦, 就没⽤了
2019-07-02 09:16

19讲如何用协程来优化多线程业务 - 图22nightmare
终于弄懂协程和线程的区别了,协程基于N:M的线程模型实现,M个协程被调度器调度,实际上也是被内核线程执⾏,不过由于需要的内核线程少,⼀个内核线程可以执⾏多个协程,占⽤资源少,⽽且上下⽂切换也更加少,⽽基于线程的1:1模型只有有阻塞就会有上下⽂切换
2019-07-02 09:09
19讲如何用协程来优化多线程业务 - 图23听⾬
⽼师,读了今天的内容,我理解的意思是:
1.因为每个轻量级线程都有⼀个内核线程⽀持,⽽java中,每个⽤户线程对应⼀个轻量级线程,可以看作⽤户线程和⽀持轻量级线程的内核线程是⼀对⼀的,所以就说java线程模型是⽤户线程和内核线程⼀对⼀。
2.那这⾥轻量级线程属于内核线程吗,我看⽂中说的是由内核线程clone⽽来的,那它算内核线程吗? 请⽼师解答⼀下!
2019-07-02 08:51
作者回复
1、对的
2、属于⽤户线程,与内核线程⼀对⼀映射
2019-07-02 10:47

19讲如何用协程来优化多线程业务 - 图24Liam
请问mailbox是如何实现阻塞的呢?
2019-07-02 08:37
作者回复
mailbox不会阻塞,这是⼀个信箱,协程之间通过mailbox来分享共享变量
2019-07-02 10:46

19讲如何用协程来优化多线程业务 - 图25沧颜
kotlin的协程设计应该和这个框架差不多吧
2019-07-02 08:14
作者回复
kotlin也很多⼈使⽤
2019-07-02 10:44