并发编程-协程

一 理解协程

协程:也称为纤程(Coroutine), 是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行。

协程与进程、线程相比并不是一个维度的概念,协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。

正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程(目前的协程框架一般都是设计成 1:N 模式)。

注意:

  • 多个进程或一个进程内的多个线程是可以并行运行的
  • 一个线程内的多个协程却是串行的,无论CPU有多少个核,因为协程本质上还是一个函数,当一个协程运行时,其它协程必须挂起
  • 但是协程的切换过程只有用户态,即没有陷入内核态,因此切换效率比进程和线程高很多

协程自己会主动适时的让出 CPU,也就是说每个协程池里面有一个调度器,这个调度器是被动调度的。意思就是他不会主动调度。而且当一个协程发现自己执行不下去了(比如异步等待网络的数据回来,但是当前还没有数据到),这个时候就可以由这个协程通知调度器,这个时候执行到调度器的代码,调度器根据事先设计好的调度算法找到当前最需要 CPU 的协程。切换这个协程的 CPU 上下文把 CPU 的运行权交个这个协程,直到这个协程出现执行不下去需要等等的情况,或者它调用主动让出 CPU 的 API 之类,触发下一次调度。

二 协程的优缺点

优点:

  • 占用小:协程更加轻量,创建成本更小,降低了内存消耗,协程一般只占据极小的内存(2~5KB),而线城市1MB左右。虽然线程和协程都是独有栈,但是线程栈是固定的,比如在Java中,基本是2M,假如一个栈只有一个打印方法,还要为此开辟一个2M的栈,就太浪费了。而Go的的协程具备动态收缩功能,初始化为2KB,最大可达1GB
  • 运行效率高:线程切换需要从用户态->内核态->用户态,而协程切换是在用户态上,即用户态->用户态->用户态,其切换过程由语言层面的调度器(coroutine)或者语言引擎(goroutine)实现。
  • 减少了同步锁:协程最终还是运行在线程上,本质上还是单线程运行,没有临界区域的话自然不需要锁的机制。多协程自然没有竞争关系。但是,如果存在临界区域,依然需要使用锁,协程可以减少以往必须使用锁的场景
  • 同步代码思维写出异步代码

缺点:

  • 无法利用多核资源:协程运行在线程上,单线程应用无法很好的利用多核,只能以多进程方式启动。
  • 协程不能有阻塞操作:线程是抢占式,线程在遇见IO操作时候,线程从运行态→阻塞态,释放cpu使用权。这是由操作系统调度。协程是非抢占式,如果遇见IO操作时候,协程是主动释放执行权限的,如果无法主动释放,程序将阻塞,无法往下执行,随之而来是整个线程被阻塞。
  • CPU密集型不是长处:假设这个线程中有一个协程是 CPU 密集型的他没有 IO 操作,也就是自己不会主动触发调度器调度的过程,那么就会出现其他协程得不到执行的情况,所以这种情况下需要程序员自己避免。

应用场景:

  • 高性能计算,牺牲公平性换取吞吐。协程最早来自高性能计算领域的成功案例,协作式调度相比抢占式调度而言,可以在牺牲公平性时换取吞吐
  • IO Bound 的任务:虽然异步IO在数据到达的时候触发回调,减少了线程切换带来性能损失,但是该思想不符合人类的思维模式。异步回调在破坏点思维连贯性的同时也破坏掉了程序的连贯性,让你在阅读程序的时候花费更多的精力。但是协程可以很好解决这个问题。比如把一个 IO 操作 写成一个协程。当触发 IO 操作的时候就自动让出 CPU 给其他协程。要知道协程的切换很轻的。协程通过这种对异步 IO 的封装既保留了性能也保证了代码的容易编写和可读性。

三 协程的简单实现

ES6提供了一种新的方法名叫Generator。Generator的执行过程可以被暂停和恢复,所以它被认为是ES6中的协程,但严格地说,Generator只是半协程(semi-coroutine),因为虽然它可以主动放弃执行权,但是它并没有告知运行环境,下一步哪个协程会被调用。当一个Generator被调用时,它的代码并不会被执行,调用者得到的是它的观察者(Observer)。调用者通过调用这个观察者的方法,比如next方法,来执行Generator的代码。

const Q = [];
const Q_LEN = 10;

function* produce() {
    while (Q.length < Q_LEN) {
        const item = Date.now();
        Q.push(item);
        console.log(`Item ${item} is produced`);
        if (Q.length === Q_LEN) {
            yield;
        }
    }
}

function* consume() {
    while (Q.length > 0) {
        const item = Q.pop();
        console.log(`Item ${item} is consumed`);
        if (Q.length === 0) {
            yield;
        }
    }
}

function bootstrap() {
    const producer = produce();
    const consumer = consume();
    while(true) {
        producer.next();
        consumer.next();
    }
}
bootstrap();

在上面代码中,produce和consume是两个协程。bootstrap方法是这两个协程的调用者,它首先获取produce和consume协程的观察者,然后循环调用观察者的next方法,从而使得生产者和消费者的关系持续运行。在循环过程中,如果produce检测队列已满,它就主动放弃执行权从而被暂停,consume将获得执行权,如果consume检测队列已空,它就主动放弃执行权从而被暂停,produce将重新获得执行权。

posted @ 2020-06-13 14:52  cool2feel  阅读(589)  评论(0编辑  收藏  举报