阻塞非阻塞这种概念都是对于线程,进程这种粒度来说的,因为只有他们才是内核有感知的,协程是你内核都不知道有这事儿,是你用户自己实现的,可以怎么说,在协程的层面上,系统是阻塞在协程.
123代码,第二行看着是阻塞的写法,但是go的http包是异步的,这边在协程粒度是阻塞住了,但是线程该干嘛就干嘛去了,对于系统来说,就是非阻塞
...
resp, err := client.Do(req)
...
小谈阻塞非阻塞
对操作系统而言,所有的输入输出设备都被抽象成文件。
分清楚内核层和应用层是关键
阻塞还是非阻塞,关注的是接口调用(发出请求)后等待数据返回时的状态。被挂起无法执行其他操作的则是阻塞型的,可以被立即「抽离」去完成其他「任务」的则是非阻塞型的。
挂起就不能干事情了吗,一个线程读文件,被阻塞了,资源不是会出让.coroutine也是把,但是比如go的routine,go的调度器会把处于阻塞的的go程上的资源分配给其他go程
异步和同步关注的是任务完成时消息通知的方式。系统内核获取到的数据到底如何返回给应用层呢?这里不同类型的操作便体现的是同步和异步的区别.。同步:应用层自己去想内核询问(轮询?).异步:内核主动通知应用层数据
在异步非阻塞模型中,没有无谓的挂起、休眠与等待,也没有盲目无知的问询与检查,应用层做到不等候片刻的最大化利用自身的资源,系统内核也十分「善解人意」的在完成任务后主动通知应用层来接收任务成果。
线程对引起阻塞的系统调用会立即阻塞该线程所属的整个进程,不太懂? 协程线程调度一个主动(协作式调度) 一个被动(抢占式调度)
在算法中如果一个线程的挂起没有导致其它的线程挂起,我们就说这个算法是非阻塞的。所以不能单纯的说线程是不是阻塞的概念
Callback- 非阻塞/异步IO
非阻塞的IO,这样服务器就可以持续运转,而不需要等待,可以使用很少的线程,即使只有一个也可以。需要定期的任务可以采取定时器来触发。
举个例子来说明一下。
传统的写法:
var file = open(‘my.txt’);
var data = file.read(); //block
sleep(1);
print(data); //block
node.js的写法:
fs.open(‘my.txt’,function(err,data){
setTimeout(1000,function(){
console.log(data);
}
}); //non-block
Coroutine-协程
一个coroutine如果处于block状态,可以交出执行权,让其他的coroutine继续执行(这个是由其实现调度的,对用户透明的)。
非阻塞I/O模型协程(Coroutines)使得开发者可以采用阻塞式的开发风格,却能够实现非阻塞I/O的效果隐式事件调度
从Knuth老爷子的基本算法卷上看“子程序其实是协程的特例”。子程序是什么?子程序(英语:Subroutine, procedure, function, routine, method, subprogram),就是函数嘛!所以协程也没什么了不起的,就是种更一般意义的程序组件,那你内存空间够大,创建多少个函数还不是随你么?
协程可以通过yield来调用其它协程。通过yield方式转移执行权的协程之间不是调用者与被调用者的关系,而是彼此对称、平等的。协程的起始处是第一个入口点,在协程里,返回点之后是接下来的入口点。子例程的生命期遵循后进先出(最后一个被调用的子例程最先返回);相反,协程的生命期完全由他们的使用的需要决定。(continuation)
进程、线程、协程的关系和区别:
线程进程都是同步机制,而协程则是异步,协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态(continuation)
- 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
- 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。默认情况下,线程栈的大小为1MB
- 协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。栈内存(大概是4~5KB)
1. 进程
操作系统中最核心的概念是进程,分布式系统中最重要的问题是进程间通信。
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
进程是“程序执行的一个实例” ,担当分配系统资源的实体。进程创建必须分配一个完整的独立地址空间。
进程切换只发生在内核态,两步:1 切换页全局目录以安装一个新的地址空间 2 切换内核态堆栈和硬件上下文。 另一种说法类似:1 保存CPU环境(寄存器值、程序计数器、堆栈指针)2修改内存管理单元MMU的寄存器 3 转换后备缓冲器TLB中的地址转换缓存内容标记为无效
用户级线程主要缺点在于对引起阻塞的系统调用的调用会立即阻塞该线程所属的整个进程。内核实现线程则会导致线程上下文切换的开销跟进程一样大,所以折衷的方法是轻量级进程(Lightweight)。在linux中,一个线程组基本上就是实现了多线程应用的一组轻量级进程。我理解为 进程中存在用户线程、轻量级进程、内核线程。
2. 线程
线程是进程的一个执行流,独立执行它自己的程序代码。是操作系统能够进行运算调度的最小单位。
线程上下文一般只包含CPU上下文及其他的线程管理信息。线程创建的开销主要取决于为线程堆栈的建立而分配内存的开销,这些开销并不大。线程上下文切换发生在两个线程需要同步的时候,比如进入共享数据段。切换只CPU寄存器值需要存储,并随后用将要切换到的线程的原先存储的值重新加载到CPU寄存器中去。
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
3. 协程
函数其实是协程的特例
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
协程可以通过yield来调用其它协程。通过yield方式转移执行权的协程之间不是调用者与被调用者的关系,而是彼此对称、平等的。协程的起始处是第一个入口点,在协程里,返回点之后是接下来的入口点。子例程的生命期遵循后进先出(最后一个被调用的子例程最先返回);相反,协程的生命期完全由他们的使用的需要决定。
协程和线程
本质上协程就是用户空间下的线程。协程-用户态的轻量级的线程 一旦创建完线程,你就无法决定他什么时候获得时间片,什么时候让出时间片了,你把它交给了内核。
协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力
协程编写者可以有一是可控的切换时机,二是很小的切换代价。从操作系统有没有调度权上看,协程就是因为不需要进行内核态的切换,所以会使用它
协程好处
-
无需线程上下文切换的开销
-
无需原子操作锁定及同步的开销
-
状态机:在一个子例程里实现状态机,这里状态由该过程当前的出口/入口点确定;这可以产生可读性更高的代码。
-
角色模型:并行的角色模型,例如计算机游戏。每个角色有自己的过程(这又在逻辑上分离了代码),但他们自愿地向顺序执行各角色过程的中央调度器交出控制(这是合作式多任务的一种形式)。
-
产生器:它有助于输入/输出和对数据结构的通用遍历
缺点
- 不能同时将 CPU 的多个核.不过现在使用协程的语言都用到了多调度器的架构,单进程下的协程也能用多核了
说了这么多,无非是说明,coroutine 是从另一个方向演化而来,它是对 continuation 概念的简化。Lua 设计者反复提到,coroutine is one-shot semi-continuation。
其他说明
- 历史上先有协程,OS模拟多任务并发,非抢占式
- 线程能利用多核达到真正的并行计算,说线程性能不好是因为设计的不好,有大量的锁,切换,等待.同一时间只有一个协程拥有运行权,所以相当于单线程的能力
- 说协程性能好的,真正原因是瓶颈在IO上面,这时候发挥不了线程的作用
- 事件驱动,callback也挺不错
- 协程可以用同步的代码感觉 写出 异步
因为程序的使用涉及大量的计算机资源配置,把这活随意的交给用户程序,非常容易让整个系统分分钟被搞跪,资源分配也很难做到相对的公平。所以核心的操作需要陷入内核(kernel),切换到操作系统,让老大帮你来做。
有的时候碰着I/O访问,阻塞了后面所有的计算。空着也是空着,老大就直接把CPU切换到其他进程,让人家先用着。当然除了I\O阻塞,还有时钟阻塞等等.但是一切换进程得反复进入内核,置换掉一大堆状态。进程数一高,大部分系统资源就被进程切换给吃掉了。后来搞出线程的概念,这个地方阻塞了,但我还有其他地方的逻辑流可以计算,这些逻辑流是共享一个地址空间的,不用特别麻烦的切换页表、刷新TLB,只要把寄存器刷新一遍就行,能比切换进程开销少点。
如果连时钟阻塞、 线程切换这些功能我们都不需要了,自己在进程里面写一个逻辑流调度的东西。那么我们即可以利用到并发优势,又可以避免反复系统调用,还有进程切换造成的开销,分分钟给你上几千个逻辑流不费力。这就是用户态线程。
从上面可以看到,实现一个用户态线程有两个必须要处理的问题:
- 是碰着阻塞式I\O会导致整个进程被挂起;
- 是由于缺乏时钟阻塞,进程需要自己拥有调度线程的能力。如果一种实现使得每个线程需要自己通过调用某个方法,主动交出控制权。那么我们就称这种用户态线程是协作式的,即是协程。
总结一下就是IO密集型一般使用多线程或者多进程,CPU密集型一般使用多进程,强调非阻塞异步并发的一般都是使用协程,当然有时候也是需要多进程线程池结合的,或者是其他组合方式。
continuation
在计算机科学和程序设计中,延续性(continuation)是一种对程序控制流程/状态的抽象表现形式。 延续性使程序状态信息具体化,也可以理解为,一个延续性以数据结构的形式表现了程序在运行过程中某一点的计算状态,相应的数据内容可以被编程语言访问,不被运行时环境所隐藏掉。 延续性包含了当前程序的栈(包括当前周期内的所有数据,也就是本地变量),以及当前运行的位置。一个延续的实例可以在将来被用做控制流,被调用时它从所表达的状态开始恢复执行。
协程wiki
适用于实现彼此熟悉的模块:合作式多任务,迭代器,无限列表和管道。
var q := new queue
生产者协程
loop
while q is not full
create some new items
add the items to q
yield to consume
消费者协程
loop
while q is not empty
remove some items from q
use the items
yield to produce
每个协程在用yield命令向另一个协程交出控制时都尽可能做了更多的工作。放弃控制使得另一个例程从这个例程停止的地方开始,但因为现在队列被修改了所以他可以做更多事情。尽管这个例子常用来介绍多线程,实际没有必要用多线程实现这种动态:yield语句可以通过由一个协程向另一个协程直接分支的方式实现。
注意 在yield的时候,队列已经被改变了,下一个生产者(或者消费者)执行的时候,直接在这个中间状态上执行就好了.
详细比较
因为相对于子例程,协程可以有多个入口和出口点,可以用协程来实现任何的子例程。事实上,正如Knuth所说:“子例程是协程的特例。” 每当子例程被调用时,执行从被调用子例程的起始处开始;然而,接下来的每次协程被调用时,从协程返回(或yield)的位置接着执行。 因为子例程只返回一次,要返回多个值就要通过集合的形式。这在有些语言,如Forth里很方便;而其他语言,如C,只允许单一的返回值,所以就需要引用一个集合。相反地,因为协程可以返回多次,返回多个值只需要在后继的协程调用中返回附加的值即可。在后继调用中返回附加值的协程常被称为产生器。 子例程容易实现于堆栈之上,因为子例程将调用的其他子例程作为下级。相反地,协程对等地调用其他协程,最好的实现是用continuations(由有垃圾回收的堆实现)以跟踪控制流程。
与subroutine(子例程 函数?)比较
当一个subroutine被invoked,execution begins at the start,当它退出时就结束.一个subroutine的实例值返回一次,不持有状态between invocation.
coroutine可以退出通过调用其他coroutine,可能过会会回到在原coroutine调用的那个point(which may return to the point where they were invoked in the original coroutine).从coroutine的视角来看,它并没有退出,只是调用了其他的coroutine(yield).因此,一个coroutine实例holds了state,并且在调用之间会有不同.
两个coroutine yield to each other是对称的(平等的),而且不是调用和被调用的关系(caller-callee 像函数的调用栈?)
所有的subroutine都可以被认为是没有yield的coroutine
要实现subroutine只需要一个简单的栈,可以预先分配,当程序执行的时候.但是coroutine要对等的调用对方,最好的实现是使用continuation
与generators相比
Generators,也叫semicoroutines.
var q := new queue
generator produce
loop
while q is not full
create some new items
add the items to q
yield consume
generator consume
loop
while q is not empty
remove some items from q
use the items
yield produce
subroutine dispatcher
var d := new dictionary(generator → iterator)
d[produce] := start produce
d[consume] := start consume
var current := produce
loop
current := next d[current]
与相互递归相比
Using coroutines for state machines or concurrency is similar to using mutual recursion with tail calls
coroutine 使用yield而不是返回,可以复用execution,而不是重头开始运行.
递归必须使用共享变量或者参数传入状态,每一个递归的调用都需要一个新的栈帧.而在coroutine之间的passing control可以使用现存的contexts,可以简单的被实现为一个jump
coroutines yield rather than return, and then resume execution rather than restarting from the beginning, they are able to hold state, both variables (as in a closure) and execution point, and yields are not limited to being in tail position;