Callback与coroutine 协程概念说明

小谈阻塞非阻塞

阻塞非阻塞概念都是对于线程, 进程这种粒度来讲的, 由于只有他们才是内核有感知的, 协程是你内核无感知, 是你用户本身实现的.java

例如在 Golang 中, resp, err := client.Do(req) 看着是阻塞的写法(有网络 IO), 可是 Go 的 Http 包是异步的, 这边在协程粒度是阻塞住了, 可是线程该干吗就干吗去了, 对于系统来讲, 就是非阻塞. 应用程序以为我遇到阻塞, 好比 I/O什么的, 我就 yield 出去, 把控制权交出去.程序员

分清楚内核层和应用层是关键web

阻塞仍是非阻塞 应用程序的调用是否当即返回. 被挂起没法执行其余操做的则是阻塞型的, 能够被当即「抽离」去完成其余「任务」的则是非阻塞型的. 算法

可是挂起就不能干事情了吗, 答案是否认的, 一个线程读文件,被阻塞了,资源会出让(陈力就列, 不能者止). coroutine也是, 可是好比 goroutine, go的调度器会把处于阻塞的的go程上的资源分配给其余go程. 可是这里的重点就是线程切换的代价比协程切换的代价高不少(线程切换涉及到内核态和用户态的切换)编程

协程线程 调度一个主动(协做式调度, 应用程序本身调度) 一个被动(抢占式调度, 操做系统调度)缓存

异步和同步 数据 copy 时进程是否阻塞. 同步:应用层本身去想内核询问(轮询?); 异步:内核主动通知应用层数据. IO 操做分为两个过程:内核等待事件, 把数据拷贝到用户缓冲区. 这两个过程只要等待任何一个都是同步 IO安全

在异步非阻塞模型中, 没有无谓的挂起、休眠与等待, 也没有盲目无知的问询与检查, 应用层作到不等候片刻的最大化利用自身的资源, 系统内核也十分「善解人意」的在完成任务后主动通知应用层来接收任务成果.服务器

进程、线程、协程

1. 进程

操做系统中最核心的概念是进程, 分布式系统中最重要的问题是进程间通讯.网络

进程是具备必定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位. 每一个进程都有本身的独立内存空间, 不一样进程经过进程间通讯来通讯. 因为进程比较重量, 占据独立的内存, 因此上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大, 但相对比较稳定安全.数据结构

进程是“程序执行的一个实例” , 担当分配系统资源的实体. 进程建立必须分配一个完整的独立地址空间.

进程切换只发生在内核态, 两步:1. 切换页全局目录以安装一个新的地址空间 2. 切换内核态堆栈和硬件上下文. 另外一种说法相似:1 保存CPU环境(寄存器值、程序计数器、堆栈指针); 2. 修改内存管理单元MMU的寄存器; 3. 转换后备缓冲器TLB中的地址转换缓存内容标记为无效

事件模型

基于事件的模型, 一个进程处理多个请求, 而且经过epoll机制来通知用户请求完成, 事件驱动适合于IO密集型服务, 多进程或线程适合于CPU密集型服务

Nginx 在一个工做进程中处理多个链接和请求, 由于满负载进程的数量不多(一般每核CPU只有一个)并且恒定, 因此任务切换只消耗不多的内存, 并且不会浪费CPU周期, 固然前提是没有阻塞

为何不使用不少进程: 每一个进程都消耗额外的内存, 并且每次进程间的切换都会消耗CPU周期并丢弃CPU高速缓存中的数据(全部处理过程是在一个简单的循环中, 由一个线程完成)

1.1 建立进程的过程

  1. 建立一个PCB
  2. 赋予一个统一进程标识符
  3. 为进程映象分配空间
  4. 初始化进程控制块
  5. 许多默认值 (如: 状态为 New, 无I/O设备或文件...)
  6. 设置相应的连接. 如: 把新进程加到就绪队列的链表中

1.2 进程切换步骤(PCB/TLB):

  1. 保存被中断进程的处理器现场信息
  2. 修改被中断进程的进程控制块的有关信息, 如进程状态等
  3. 把被中断进程的PCB加入有关队列
  4. 选择下一个占有处理器运行的进程
  5. 修改被选中进程的PCB的有关信息
  6. 根据被选中进程设置操做系统用到的地址转换和存储保护信息
  7. 根据被选中进程恢复处理器现场

进程上下文:

进程上下文:操做系统中把进程物理实体和支持进程运行的环境合称为进程上下文(context). 进程实体+运行环境.

  • 用户级上下文:由用户程序块、用户数据块和用户堆栈组成的进程地址空间.
  • 系统级上下文:由进程控制块、内存管理信息、进程环境块, 及系统堆栈等组成的进程地址空间.
  • 寄存器上下文:由PSW寄存器和各种控制寄存器、地址寄存器、通用寄存器组成、用户栈指针等组成.

进程阻塞过程

中止当前进程的执行;保存该进程的CPU现场信息;将进程状态改成阻塞态, 并将其PCB入相应的阻塞队列;转进程调度程序.

进程唤醒过程

首先把被阻塞的进程从等待该事件的阻塞队列中移出, 将其PCB中的现行状态由阻塞改成就绪, 而后再将该PCB插入到就绪队列中. 进程切换:中断处于运行态的进程运行, 让出处理器, 恢复新进程的状态, 使新进程投入运行. 当系统调度新进程占有处理器时, 新老进程随之发生上下文切换. 进程的运行被认为是在进程的上下文中执行的.

2. 线程

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程本身基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),可是它可与同属一个进程的其余的线程共享进程所拥有的所有资源. 线程间通讯主要经过共享内存, 上下文切换很快, 资源开销较少, 但相比进程不够稳定容易丢失数据.

一旦建立完线程, 你就没法决定他何时得到时间片, 何时让出时间片了, 你把它交给了内核. 由于程序的使用涉及大量的计算机资源配置, 把这活随意的交给用户程序, 很是容易让整个系统分分钟被搞跪, 资源分配也很难作到相对的公平. 因此核心的操做须要陷入内核(kernel), 切换到操做系统, 让老大帮你来作.

线程上下文通常只包含CPU上下文及其余的线程管理信息. 线程建立的开销主要取决于为线程堆栈的创建而分配内存的开销, 这些开销并不大. 线程上下文切换发生在两个线程须要同步的时候, 好比进入共享数据段. 切换只CPU寄存器值须要存储, 并随后用将要切换到的线程的原先存储的值从新加载到CPU寄存器中去.

有的时候碰着I/O访问, 阻塞了后面全部的计算. 空着也是空着, 老大就直接把CPU切换到其余进程, 让人家先用着. 固然除了I\O阻塞, 还有时钟阻塞等等.可是一切换进程得反复进入内核, 置换掉一大堆状态. 进程数一高, 大部分系统资源就被进程切换给吃掉了. 后来搞出线程的概念, 这个地方阻塞了, 但我还有其余地方的逻辑流能够计算, 这些逻辑流是共享一个地址空间的, 不用特别麻烦的切换页表、刷新TLB, 只要把寄存器刷新一遍就行, 能比切换进程开销少点.

3. 协程

一个Coroutine若是处于block状态, 能够交出执行权, 让其余的coroutine继续执行(这个是由其实现调度的,对用户透明的).

Coroutines使得开发者能够采用阻塞式的开发风格,却可以实现非阻塞I/O的效果隐式事件调度

函数实际上是协程的特例, 从Knuth老爷子的基本算法卷上看“子程序实际上是协程的特例”. 子程序是什么?子程序(英语:Subroutine, procedure, function, routine, method, subprogram), 就是函数嘛!因此协程也没什么了不得的, 就是种更通常意义的程序组件, 那你内存空间够大, 建立多少个函数还不是随你么?

协程能够经过yield来调用其它协程. 经过yield方式转移执行权的协程之间不是调用者与被调用者的关系, 而是彼此对称、平等的. 协程的起始处是第一个入口点, 在协程里, 返回点以后是接下来的入口点. 子例程的生命期遵循后进先出(最后一个被调用的子例程最早返回);相反, 协程的生命期彻底由他们的使用的须要决定. (continuation)

协程是一种用户态的轻量级线程, 协程的调度彻底由用户控制. 协程拥有本身的寄存器上下文和栈. 协程调度切换时, 将寄存器上下文和栈保存到其余地方, 在切回来的时候, 恢复先前保存的寄存器上下文和栈, 直接操做栈则基本没有内核切换的开销, 能够不加锁的访问全局变量, 因此上下文的切换很是快.

协程能够经过yield来调用其它协程. 经过yield方式转移执行权的协程之间不是调用者与被调用者的关系, 而是彼此对称、平等的. 协程的起始处是第一个入口点, 在协程里, 返回点以后是接下来的入口点. 子例程的生命期遵循后进先出(最后一个被调用的子例程最早返回);相反, 协程的生命期彻底由他们的使用的须要决定.

协程和线程的区别是:协程避免了无心义的调度, 由此能够提升性能, 但也所以, 程序员必须本身承担调度的责任, 同时, 协程也失去了标准线程使用多CPU的能力

协程编写者能够有一是可控的切换时机, 二是很小的切换代价. 从操做系统有没有调度权上看, 协程就是由于不须要进行内核态的切换, 因此会使用它

协程好处

  • 无需线程上下文切换的开销

  • 无需原子操做锁定及同步的开销

  • 状态机:在一个子例程里实现状态机, 这里状态由该过程当前的出口/入口点肯定;这能够产生可读性更高的代码.

  • 角色模型:并行的角色模型, 例如计算机游戏. 每一个角色有本身的过程(这又在逻辑上分离了代码), 但他们自愿地向顺序执行各角色过程的中央调度器交出控制(这是合做式多任务的一种形式).

  • 产生器:它有助于输入/输出和对数据结构的通用遍历

缺点

  • 不能同时将 CPU 的多个核.不过如今使用协程的语言都用到了多调度器的架构, 单进程下的协程也能用多核了

说了这么多, 无非是说明, coroutine 是从另外一个方向演化而来, 它是对 continuation 概念的简化. Lua 设计者反复提到, coroutine is one-shot semi-continuation.

其余说明

  1. 历史上先有协程,OS模拟多任务并发, 非抢占式
  2. 线程能利用多核达到真正的并行计算, 说线程性能很差是由于设计的很差, 有大量的锁/切换/等待. 同一时间只有一个协程拥有运行权, 因此至关于单线程的能力
  3. 说协程性能好的,真正缘由是瓶颈在IO上面,这时候发挥不了线程的做用
  4. 事件驱动, Callback也挺不错
  5. 协程能够用同步的代码感受 写出异步的效果

分割线


进程、线程、协程的关系和区别:

方面: cpu调度, 上下文切换, 数 据共享, 多核cup利用率, 资源占用角度.

线程进程都是同步机制, 而协程则是异步,协程能保留上一次调用时的状态, 每次过程重入时, 就至关于进入上一次调用的状态(continuation)

线程和进程主要区别是在轻量级和重量级, 他们调度都是系统来的; 协程和线程的区别是在调度:协程避免了无心义的调度, 由此能够提升性能, 但也所以, 程序员必须本身承担调度的责任, 同时, 协程也失去了标准线程使用多CPU的能力.

  • 进程拥有本身独立的堆和栈, 既不共享堆, 亦不共享栈, 进程由操做系统调度. 一个进程死亡对其余进程没有影响

  • 线程拥有本身独立的栈和共享的堆, 共享堆, 不共享栈, 线程亦由操做系统调度(标准线程是的), 没有本身独立的地址空间!!! 默认状况下, 线程栈的大小为1MB. 线程私有栈, 一个线程挂掉将致使整个进程挂掉, 由于线程没有本身单独的内存地址空间. 当一个线程向非法地址读取或者写入(伴随栈溢出、读取或者访问了非法地址), 没法确认这个操做是否会影响同一进程中的其它线程, 因此只能是整个进程一块儿崩溃. (注意 这个崩溃不是 java 的异常哦, jvm 作了不少保护)

  • 协程和线程同样共享堆, 不共享栈, 协程由程序员在协程的代码里显示调度. 栈内存(大概是4~5KB)

  1. 须要频繁建立销毁的优先用线程. 实例:web服务器. 来一个创建一个线程, 断了就销毁线程. 要是用进程, 建立和销毁的代价是很难承受的.

  2. 须要进行大量计算的优先使用线程. 所谓大量计算, 固然就是要消耗不少cpu, 切换频繁了, 这种状况先线程是最合适的. 实例:图像处理、算法处理.

  3. 强相关的处理用线程, 弱相关的处理用进程 什么叫强相关、弱相关?理论上很难定义, 给个简单的例子就明白了. 通常的server须要完成以下任务:消息收发和消息处理. 消息收发和消息处理就是弱相关的任务, 而消息处理里面可能又分为消息解码、业务处理, 这两个任务相对来讲相关性就要强多 了. 所以消息收发和消息处理能够分进程设计, 消息解码和业务处理能够分线程设计.

  4. 可能扩展到多机分布的用进程, 多核分布的用线程

总结一下就是IO密集型通常使用多线程或者多进程, CPU密集型通常使用多进程, 强调非阻塞异步并发的通常都是使用协程, 固然有时候也是须要多进程线程池结合的, 或者是其余组合方式. Nginx 用一个进程能够跑满整个机器的 cpu(都是非阻塞的操做, 没有比这更快的了, Nginx 最近也引入了线程池, 对付会阻塞的任务),


分割线


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;

(好久之前的博客了, 不是原创, 参考了不少资料, 加着本身的理解)

相关文章
相关标签/搜索