转自:http://jolestar.com/parallel-programming-model-thread-goroutine-actor/java
先梳理下两个概念,几乎全部讲并发的文章都要先讲这两个概念:linux
因此总结下,并发并不要求必须并行,能够用时间片切分的方式模拟,好比单核cpu上的多任务系统,并发的要求是任务能切分红独立执行的片断。而并行关注的是同时执行,必须是多(核)cpu,要能并行的程序必须是支持并发的。本文大多数状况下不会严格区分这两个概念,默认并发就是指并行机制下的并发。golang
We believe that writing correct concurrent, fault-tolerant and scalable applications is too hard. Most of the time it’s because we are using the wrong tools and the wrong level of abstraction. —— Akka正则表达式
Akka官方文档开篇这句话说的好,之因此写正确的并发,容错,可扩展的程序如此之难,是由于咱们用了错误的工具和错误的抽象。(固然该文档原本的意思是Akka是正确的工具,但咱们能够独立的看待这句话)。数据库
那咱们从最开始梳理下程序的抽象。开始咱们的程序是面向过程的,数据结构+func。后来有了面向对象,对象组合了数结构和func,咱们想用模拟现实世界的方式,抽象出对象,有状态和行为。但不管是面向过程的func仍是面向对象的func,本质上都是代码块的组织单元,自己并无包含代码块的并发策略的定义。因而为了解决并发的需求,引入了Thread(线程)的概念。编程
线程(Thread)安全
线程的出现解决了两个问题,一个是GUI出现后急切须要并发机制来保证用户界面的响应。第二是互联网发展后带来的多用户问题。最先的CGI程序很简单,将经过脚本将原来单机版的程序包装在一个进程里,来一个用户就启动一个进程。但明显这样承载不了多少用户,而且若是进程间须要共享资源还得经过进程间的通讯机制,线程的出现缓解了这个问题。服务器
线程的使用比较简单,若是你以为这块代码须要并发,就把它放在单独的线程里执行,由系统负责调度,具体何时使用线程,要用多少个线程,由调用方决定,但定义方并不清楚调用方会如何使用本身的代码,不少并发问题都是由于误用致使的,好比Go中的map以及Java的HashMap都不是并发安全的,误用在多线程环境就会致使问题。另外也带来复杂度:网络
为了解决上述问题,咱们引入了许多复杂机制来保证:数据结构
若是说上面两个问题只是增长了复杂度,咱们经过深刻学习,严谨的CodeReview,全面的并发测试(好比Go语言中单元测试的时候加上-race参数),必定程度上能解决(固然这个也是有争议的,有论文认为当前的大多数并发程序没出问题只是并发度不够,若是CPU核数继续增长,程序运行的时间更长,很难保证不出问题)。但最让人头痛的仍是下面这个问题:
系统里到底须要多少线程?
这个问题咱们先从硬件资源入手,考虑下线程的成本:
调度成本(context-switch)
我在我的电脑上作的一个非严格测试,模拟两个线程互相唤醒轮流挂起,线程切换成本大约6000纳秒/次。这个还没考虑栈空间大小的影响。国外一篇论文专门分析线程切换的成本,基本上得出的结论是切换成本和栈空间使用大小直接相关。
这个咱们能够经过一个公式计算出来,100/(15+5)*4=20,用20个线程最合适。但一方面网络的时间不是固定的,另一方面,若是考虑到其余瓶颈资源呢?好比锁,好比数据库链接池,就会更复杂。
做为一个1岁多孩子的父亲,认为这个问题的难度比如你要写个给孩子喂饭的程序,须要考虑『给孩子喂多少饭合适?』,这个问题有如下回答以及策略:
经过这个例子咱们能够看出,从外部系统来观察,或者以经验的方式进行计算,都是很是困难的。因而结论是:
让孩子会说话,吃饱了本身说,本身学会吃饭,自管理是最佳方案。
然并卵,计算机不会本身说话,如何自管理?
但咱们从以上的讨论能够得出一个结论:
Java1.5后,Doug Lea的Executor系列被包含在默认的JDK内,是典型的线程池方案。
线程池必定程度上控制了线程的数量,实现了线程复用,下降了线程的使用成本。但仍是没有解决数量的问题,线程池初始化的时候仍是要设置一个最小和最大线程数,以及任务队列的长度,自管理只是在设定范围内的动态调整。另外不一样的任务可能有不一样的并发需求,为了不互相影响可能须要多个线程池,最后致使的结果就是Java的系统里充斥了大量的线程池。
从前面的分析咱们能够看出,若是线程是一直处于运行状态,咱们只需设置和CPU核数相等的线程数便可,这样就能够最大化的利用CPU,而且下降切换成本以及内存使用。但如何作到这一点呢?
陈力就列,不能者止
这句话是说,能干活的代码片断就放在线程里,若是干不了活(须要等待,被阻塞等),就摘下来。通俗的说就是不要占着茅坑不拉屎,若是拉不出来,须要酝酿下,先把茅坑让出来,由于茅坑是稀缺资源。
要作到这点通常有两种方案:
异步回调方案 典型如NodeJS,遇到阻塞的状况,好比网络调用,则注册一个回调方法(其实还包括了一些上下文数据对象)给IO调度器(linux下是libev,调度器在另外的线程里),当前线程就被释放了,去干别的事情了。等数据准备好,调度器会将结果传递给回调方法而后执行,执行其实不在原来发起请求的线程里了,但对用户来讲无感知。但这种方式的问题就是很容易遇到callback hell,由于全部的阻塞操做都必须异步,不然系统就卡死了。还有就是异步的方式有点违反人类思惟习惯,人类仍是习惯同步的方式。
GreenThread/Coroutine/Fiber方案 这种方案其实和上面的方案本质上区别不大,关键在于回调上下文的保存以及执行机制。为了解决回调方法带来的难题,这种方案的思路是写代码的时候仍是按顺序写,但遇到IO等阻塞调用时,将当前的代码片断暂停,保存上下文,让出当前线程。等IO事件回来,而后再找个线程让当前代码片断恢复上下文继续执行,写代码的时候感受好像是同步的,仿佛在同一个线程完成的,但实际上系统可能切换了线程,但对程序无感。
GreenThread
几个概念
Goroutine其实就是前面GreenThread系列解决方案的一种演进和实现。
Goroutine调度器
这个图通常讲Goroutine调度器的地方都会引用,想要仔细了解的能够看看原博客。这里只说明几点:
Goroutine是银弹么?
Goroutine很大程度上下降了并发的开发成本,是否是咱们全部须要并发的地方直接go func就搞定了呢?
Go经过Goroutine的调度解决了CPU利用率的问题。但遇到其余的瓶颈资源如何处理?好比带锁的共享资源,好比数据库链接等。互联网在线应用场景下,若是每一个请求都扔到一个Goroutine里,当资源出现瓶颈的时候,会致使大量的Goroutine阻塞,最后用户请求超时。这时候就须要用Goroutine池来进行控流,同时问题又来了:池子里设置多少个Goroutine合适?
因此这个问题仍是没有从更本上解决。
Actor对没接触过这个概念的人可能不太好理解,Actor的概念其实和OO里的对象相似,是一种抽象。面对对象编程对现实的抽象是对象=属性+行为(method),但当使用方调用对象行为(method)的时候,其实占用的是调用方的CPU时间片,是否并发也是由调用方决定的。这个抽象其实和现实世界是有差别的。现实世界更像Actor的抽象,互相都是经过异步消息通讯的。好比你对一个美女say hi,美女是否回应,如何回应是由美女本身决定的,运行在美女本身的大脑里,并不会占用发送者的大脑。
因此Actor有如下特征:
Actor遵循如下规则:
Actor的目标:
Actor的实现:
两者的格言都是:
Don’t communicate by sharing memory, share memory by communicating
经过消息通讯的机制来避免竞态条件,但具体的抽象和实现上有些差别。
从这样看来,CSP的模式比较适合Boss-Worker模式的任务分发机制,它的侵入性没那么强,能够在现有的系统中经过CSP解决某个具体的问题。它并不试图解决通讯的超时容错问题,这个仍是须要发起方进行处理。同时因为Channel是显式的,虽然能够经过netchan(原来Go提供的netchan机制因为过于复杂,被废弃,在讨论新的netchan)实现远程Channel,但很难作到对使用方透明。而Actor则是一种全新的抽象,使用Actor要面临整个应用架构机制和思惟方式的变动。它试图要解决的问题要更广一些,好比容错,好比分布式。但Actor的问题在于以当前的调度效率,哪怕是用Goroutine这样的机制,也很难达到直接方法调用的效率。当前要像OO的『一切皆对象』同样实现一个『一切皆Actor』的语言,效率上确定有问题。因此折中的方式是在OO的基础上,将系统的某个层面的组件抽象为Actor。
Rust解决并发问题的思路是首先认可现实世界的资源老是有限的,想完全避免资源共享是很难的,不试图彻底避免资源共享,它认为并发的问题不在于资源共享,而在于错误的使用资源共享。好比咱们前面提到的,大多数语言定义类型的时候,并不能限制调用方如何使用,只能经过文档或者标记的方式(好比Java中的@ThreadSafe ,@NotThreadSafe annotation)说明是否并发安全,但也只能仅仅作到提示的做用,不能阻止调用方误用。虽然Go提供了-race机制,能够经过运行单元测试的时候带上这个参数来检测竞态条件,但若是你的单元测试并发度不够,覆盖面不到也检测不出来。因此Rust的解决方案就是:
有了这机制,Rust能够在编译期而不是运行期对竞态条件作检查和限制。虽然开发的时候增长了心智成本,但下降了调用方以及排查并发问题的心智成本,也是一种有特点的解决方案。
革命还没有成功 同志任需努力
本文带你们一块儿回顾了并发的问题,和各类解决方案。虽然各家有各家的优点以及使用场景,但并发带来的问题还远远没到解决的程度。因此还需努力,你们也有机会啊。
这个咱们能够经过一个公式计算出来,100/(15+5)*4=20,用20个线程最合适。但一方面网络的时间不是固定的,另一方面,若是考虑到其余瓶颈资源呢?好比锁,好比数据库链接池,就会更复杂。
做为一个1岁多孩子的父亲,认为这个问题的难度比如你要写个给孩子喂饭的程序,须要考虑『给孩子喂多少饭合适?』,这个问题有如下回答以及策略:
经过这个例子咱们能够看出,从外部系统来观察,或者以经验的方式进行计算,都是很是困难的。因而结论是:
让孩子会说话,吃饱了本身说,本身学会吃饭,自管理是最佳方案。
然并卵,计算机不会本身说话,如何自管理?
但咱们从以上的讨论能够得出一个结论:
Java1.5后,Doug Lea的Executor系列被包含在默认的JDK内,是典型的线程池方案。
线程池必定程度上控制了线程的数量,实现了线程复用,下降了线程的使用成本。但仍是没有解决数量的问题,线程池初始化的时候仍是要设置一个最小和最大线程数,以及任务队列的长度,自管理只是在设定范围内的动态调整。另外不一样的任务可能有不一样的并发需求,为了不互相影响可能须要多个线程池,最后致使的结果就是Java的系统里充斥了大量的线程池。
从前面的分析咱们能够看出,若是线程是一直处于运行状态,咱们只需设置和CPU核数相等的线程数便可,这样就能够最大化的利用CPU,而且下降切换成本以及内存使用。但如何作到这一点呢?
陈力就列,不能者止
这句话是说,能干活的代码片断就放在线程里,若是干不了活(须要等待,被阻塞等),就摘下来。通俗的说就是不要占着茅坑不拉屎,若是拉不出来,须要酝酿下,先把茅坑让出来,由于茅坑是稀缺资源。
要作到这点通常有两种方案:
异步回调方案 典型如NodeJS,遇到阻塞的状况,好比网络调用,则注册一个回调方法(其实还包括了一些上下文数据对象)给IO调度器(linux下是libev,调度器在另外的线程里),当前线程就被释放了,去干别的事情了。等数据准备好,调度器会将结果传递给回调方法而后执行,执行其实不在原来发起请求的线程里了,但对用户来讲无感知。但这种方式的问题就是很容易遇到callback hell,由于全部的阻塞操做都必须异步,不然系统就卡死了。还有就是异步的方式有点违反人类思惟习惯,人类仍是习惯同步的方式。
GreenThread/Coroutine/Fiber方案 这种方案其实和上面的方案本质上区别不大,关键在于回调上下文的保存以及执行机制。为了解决回调方法带来的难题,这种方案的思路是写代码的时候仍是按顺序写,但遇到IO等阻塞调用时,将当前的代码片断暂停,保存上下文,让出当前线程。等IO事件回来,而后再找个线程让当前代码片断恢复上下文继续执行,写代码的时候感受好像是同步的,仿佛在同一个线程完成的,但实际上系统可能切换了线程,但对程序无感。
GreenThread
几个概念
Goroutine其实就是前面GreenThread系列解决方案的一种演进和实现。
Goroutine调度器