我是一个协程,我生活在线程里。虽然我出生几十年了,但还是个“屌丝”。
不过我有自己的梦想,就是有一天能像线程一样牛逼。因为江湖中有人说过,人总是要有梦想的,万一实现了呢。
其实我也知道,这只是牛人说的安慰芸芸菜鸡的话而已,所以我还是先面对现实吧。
随便在网上一搜,关于我的消息还是挺多的,但实际当中使用我的人和项目并不多,可能是主流语言都没有对我进行很好的支持吧。
所以网上文章几乎都是“纸上谈兵”,都是理论知识,有这样说的,有那样说的,看起来好像都对。
说实话,其实连我自己都不太了解我自己,所以才默默无闻几十载,还未出人头地。
我看大家在谈论的时候,都会拿我去和函数、线程、进程进行对比,那我今天也采用相似的方式来谈谈我自己。
这叫主人公现身说法,哈哈,而且我还把很多“当事人”,他们都是我的好朋友,也都请到了现场。
其中进程是一个资深大牛,也是历史爱好者,就由他先来发言吧。 掌声过后,进程开始了。
他说,我们这一帮人之所以存在啊,都是因为有一个叫计算机的家伙。 那么为什么会有这个计算机呢?
其实是当年美国在研发原子弹的时候,设计出了v1.0版的引爆方式,但是需要先从理论上计算下是否可行。
但是这个计算量实在太大,于是就发明了一个会计算的机器。 很可惜,结果是不能引爆。
于是又设计了v2.0版的引爆方式,理论计算结果可行,实际就真的引爆了。
这告诉我们一个道理,无论什么事物,v1.0版基本是靠不住的。 咳咳,好像有点跑偏了,赶紧言归正传。
不管怎么样吧,那个大机器早已经不可与今天的计算机同日而语了。
现在的计算机都有操作系统,而且可以同时运行多个进程,每个进程其实都相当于一个大的任务。
进程与进程之间是互相隔离的,一方面从稳定的角度来说,一个进程的崩溃不能影响到别的进程。
另一方面从隐私的角度来说,一个进程里的数据不能被别的进行访问到,万一是明星们的特殊照片,那微博就要再遭受一次打击了。
所以进程是操作系统在分配资源时使用的单位。 资源分配好后,真正执行的事情就交给了线程。
下面就请线程老弟给大家讲吧,掌声落后,线程开始了。
进程兄说的没错,线程是操作系统在执行程序时的单位。 所以一个进程中至少要有一个线程,通常称为主线程。
实际当中为了使程序能运行的更好,一个进程中一般会有多个线程,这些线程共享这个进程的资源。
这时就会出现多个线程同时访问同一资源的竞争情况,当然可以通过加锁来解决,但加锁会影响执行效率,所以尽量减少锁的使用。
此外,CPU的一个核心一次只能运行一个线程,那这么多的线程只能轮流执行,这就涉及到频繁的线程上下文切换。
关键是线程的切换是比较昂贵的,这要从操作系统和程序语言的设计来说了。
上面说了进程和进程之间是不能互相影响的,其实运行在操作系统上的程序和操作系统本身之间也是不能随意影响的。
所以操作系统在设计时分为内核空间和用户空间。 内核空间设计的像皇宫一样安全,四周筑起高墙并派重兵把守。 因此只有操作系统的核心可以运行在其中。
普通程序只能运行在用户空间,而用户空间和内核空间的交互非常麻烦和耗资源,就像大臣进宫见皇帝一样繁琐。
几乎程序语言里都有线程的概念,一般实现都是把它映射为操作系统里的线程。 操作系统切换线程是在内核空间完成的,而实际程序中的线程是运行在用户空间的。
这就造成两个空间的交互是不可避免的。 所以就要尽量减少线程的频繁切换。 一种做法就是减少线程的数目,但是过少的线程数目又不能完全发挥CPU的能力,因此这是一个权衡的问题。
站在线程的角度,线程的切换也分两种,一种是主动切换。 如线程在执行时遇到I/O的情况,就会主动让出CPU,因为I/O几乎不需要CPU参与。
另一种是线程的时间片用完了,它的CPU就给切出去了。 这时可以认为是被动切换。 无论哪种情况,CPU都会从其它就绪的线程中再选择一个来执行。
可以看出,多线程既有好处也有坏处,好处当然需要继承,坏处自然也要想办法解决。
下面有请协程老弟来解决一下吧,掌声过后,协程开始了。
终于轮到我上场了。 大家可以通过对比的方式来了解我。 一个进程中有多个线程,自然一个线程中就有多个协程。
多线程之间可以并发运行,提高效率,多协程之间也可以并发运行。 这样多线程的好处就被继承了过来。
多线程在访问资源时可能会出现安全问题,需要加锁。 多协程则没有这方面的问题,因为它们位于一个线程内,一个线程是不需要加锁的。
多线程的切换需要操作系统参与且在内核空间完成。 多协程的切换在用户空间完成,不需要操作系统干预,因为操作系统压根就不知道协程的存在。
这样一来,多协程就继承了多线程的好处,屏蔽了多线程的坏处,在“理想”的情况下,确实算是一种改进。
那多协程该如何切换(也称调度)呢? 我想可以参考线程的方式。 也分为主动和被动两种。
一些程序语言里使用yield关键词让协程主动让出CPU。 我想还应该有一个协程调度器在协程的时间片用完后把它切出去,这算被动让出CPU吧。
不过后面这个被动方式,可能会造成多协程间产生资源竞争的安全问题,因为一个协程正访问着资源呢,就被赶跑了,下一个协程又来了,极有可能产生问题。
所以多协程和多线程既有相似的地方,也一定有不一样的地方,并不只是简单的向下延伸一级而已。
多线程的切换,主要有操作系统的切换加上代码主动让出CPU两种形式。 由于操作系统无法感知到协程的存在,所以在协程切换中充当操作系统角色的只能是语言的VM(虚拟机,类似于Java的JVM)了。
所以协程的实现,其实是需要语言本身的完全支持的,既包括语言语法的支持,也包括语言底层的虚拟机的支持。
语法来指导程序员如何定义和使用协程,也可以认为是以更加合理的方式来运用协程。 虚拟机需要实现协程调度器,就像线程调度器那样,以防止一个协程总是老占着坑位不让别人用。
这就是我对自己的认知,是不是略有抽象,下面有请我的好哥们函数来介绍下自己,以便于更好的对我的理解。
大家好,我是函数,其实就是一段程序代码,在需要的地方可以调用一下,而不是把整段代码再抄一遍。 所以没有什么特别之处。
在主流语言中,函数的执行比较有特点,一旦函数开始执行,那么只有三种情况可以离开这个函数。
一是函数执行完毕。 二是函数抛出异常。 三是函数又调用了其它函数。
有三个函数A、B、C,假如A调用了B,B调用了C。 那么执行情况就是由A到B再到C,C执行完回到B,B执行完再回到A,A执行完就结束了。
func A() { B(); } func B() { C(); } func C() { }
有三个函数D、E、F,假如在main函数中依次调用它们,那么执行情况就是先执行D,D完了在执行E,E完了在执行F。 F完了main函数就结束了。
void main() {
D();
E();
F();
}
可以看出函数的执行行为是确定的。 现在主流程序都是从主线程里的main函数开始执行的,所以都可以看成是无数多函数之间的调用。
所以一个线程里的代码的执行路径也是确定,不会出现随机性的。 比如正执行着函数D呢,执行了一部分,又跑去执行函数E,执行了一部分,又跑回去继续执行函数D。
这种情况绝对不会出现,因为这是由底层VM确定的,因为VM只有在遇到返回、异常、调用其它函数时才会离开当前函数的栈帧。 这是规定。
但是对于协程这哥们,恰恰需要违反这种规定。 因为多个协程间需要可控的切换着轮流执行。
比如线程正在执行协程X呢,执行了一会,突然跑去执行协程Y了,执行了一会,又回到协程X继续执行了。
这种切换理论上可以随意进行,因为它们都在用户空间,而且在同一个线程里,和if/else没啥区别。
比如下面这两个协程(这只是演示代码):
coroutine X() { print('1'); print('2'); print('3'); } coroutine Y() { print('x'); print('y'); print('z'); }
在main函数中调用一下(为了简化,像函数一样调用了):
void main() { X(); Y(); }
这两个协程都运行在主线程中,但它们之间可以切换着执行,所以一个可能的结果是这样的:
1 2 x y 3 z
这样我们看到了,在一个线程中由于有了协程,代码的执行路径不再是确定的了。 也就是出现了单线程内的并发执行。
所以此时协程就变成了比线程再小一级的代码执行单元了。 这些执行单元可以在线程内并发执行,它们之间既能协作配合好又不会出现竞争问题。
感谢我的好哥们函数把我介绍的这么清晰。 由于我的执行模式与现有主流语言差别较大,因此目前都不(原生的)支持我。
总的来说,我就是比线程低一级的执行单元,可以在线程内并发执行,又不会引起安全问题。 由于操作系统只能管到线程,所以它感觉不到我的存在。
所以我需要有程序语言本身的支持,包括语法和VM,其中语法需要增加一些和我相关的关键字,VM需要增加一些和我相关的指令,甚至编译器也要辅助做一些工作。
再看个类比,如果操作系统是国家,那进程就是省,线程就是城市,我协程呢,就是县了。
理解起来不难,只是各位主流语言爸爸们,你们什么时候才考虑支持我啊,我不想再做“穷屌丝”了,我也渴望“白富美”啊。
喂、喂,醒醒,醒醒,口水都流出来了。
作者 是工作超过 10年 的码农,现在任架构师。喜欢研究技术,崇尚简单快乐。 追求以通俗易懂的语言解说技术,希望所有的读者都能看懂并记住。
>>> 热门文章集锦 <<<
毕业10年,我有话说
线程池开门营业招聘开发人员的一天
【面试】我是如何面试别人List相关知识的,深度有点长文
我是如何在毕业不久只用1年就升为开发组长的
爸爸又给Spring MVC生了个弟弟叫Spring WebFlux
【面试】我是如何在面试别人Spring事务时“套路”对方的
【面试】Spring事务面试考点吐血整理(建议珍藏)
【面试】我是如何在面试别人Redis相关知识时“软怼”他的
【面试】吃透了这些Redis知识点,面试官一定觉得你很NB(干货 | 建议珍藏)
【面试】如果你这样回答“什么是线程安全”,面试官都会对你刮目相看(建议珍藏)
【面试】迄今为止把同步/异步/阻塞/非阻塞/BIO/NIO/AIO讲的这么清楚的好文章(快快珍藏)
【面试】一篇文章帮你彻底搞清楚“I/O多路复用”和“异步I/O”的前世今生(深度好文,建议珍藏)
【面试】如果把线程当作一个人来对待,所有问题都瞬间明白了
Java多线程通关———基础知识挑战
品Spring:帝国的基石
【玩转SpringBoot】配置文件yml的正确打开姿势
【玩转SpringBoot】用好条件相关注解,开启自动配置之门
【玩转SpringBoot】给自动配置来个整体大揭秘
【玩转SpringBoot】看似复杂的Environment其实很简单
【玩转SpringBoot】翻身做主人,一统web服务器
【玩转SpringBoot】让错误处理重新由web服务器接管
【玩转SpringBoot】SpringBoot应用的启动过程一览表
【玩转SpringBoot】通过事件机制参与SpringBoot应用的启动过程
【玩转SpringBoot】异步任务执行与其线程池配置
品Spring:帝国的基石
品Spring:bean定义上梁山
品Spring:实现bean定义时采用的“先进生产力”
品Spring:注解终于“成功上位”
品Spring:能工巧匠们对注解的“加持”
品Spring:SpringBoot和Spring到底有没有本质的不同?
品Spring:负责bean定义注册的两个“排头兵”
品Spring:SpringBoot轻松取胜bean定义注册的“第一阶段”
品Spring:SpringBoot发起bean定义注册的“二次攻坚战”
品Spring:注解之王@Configuration和它的一众“小弟们”
品Spring:bean工厂后处理器的调用规则
品Spring:详细解说bean后处理器
品Spring:对@PostConstruct和@PreDestroy注解的处理方法
品Spring:对@Resource注解的处理方法
品Spring:对@Autowired和@Value注解的处理方法
品Spring:真没想到,三十步才能完成一个bean实例的创建
品Spring:关于@Scheduled定时任务的思考与探索,结果尴尬了