协程Coroutine(一)

协程是现代编程语言的一重要特性,是一种比线程更加轻量级的存在。C#,python等语言都包含了协程功能,而C++也在其最新的标准C++20中引入了协程这一特性。

1. 协程的介绍

参考:https://www.jianshu.com/p/6dde7f92951e

 

 

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

协程不是进程也不是线程,而是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行。所以说,协程与进程、线程相比并不是一个维度的概念。

一个进程可以包含多个线程,一个线程也可以包含多个协程。简单来说,一个线程内可以由多个这样的特殊函数在运行,但是有一点必须明确的是,一个线程的多个协程的运行是串行的。如果是多核CPU,多个进程或一个进程内的多个线程是可以并行运行的,但是一个线程内协程却绝对是串行的,无论CPU有多少个核。毕竟协程虽然是一个特殊的函数,但仍然是一个函数。一个线程内可以运行多个函数,但这些函数都是串行运行的。当一个协程运行时,其它协程必须挂起。

进程、线程、协程的对比

  • 协程既不是进程也不是线程,协程仅仅是一个特殊的函数,协程它进程和进程不是一个维度的。
  • 一个进程可以包含多个线程,一个线程可以包含多个协程。
  • 一个线程内的多个协程虽然可以切换,但是多个协程是串行执行的,只能在一个线程内运行,没法利用CPU多核能力。
  • 协程与进程一样,切换是存在上下文切换问题的。

上下文切换

  • 进程的切换者是操作系统,切换时机是根据操作系统自己的切换策略,用户是无感知的。进程的切换内容包括页全局目录、内核栈、硬件上下文,切换内容保存在内存中。进程切换过程是由“用户态到内核态到用户态”的方式,切换效率低。

  • 线程的切换者是操作系统,切换时机是根据操作系统自己的切换策略,用户无感知。线程的切换内容包括内核栈和硬件上下文。线程切换内容保存在内核栈中。线程切换过程是由“用户态到内核态到用户态”, 切换效率中等。

  • 协程的切换者是用户(编程者或应用程序),切换时机是用户自己的程序所决定的。协程的切换内容是硬件上下文,切换内存保存在用户自己的变量(用户栈或堆)中。协程的切换过程只有用户态,即没有陷入内核态,因此切换效率高。

 

2. Python中的实现

python中可以用yield和send实现协程。

  • 程序一旦执行到yield就会停在该处,并且将其返回值进行返回。
  • send()方法具有两种功能:第一,传值,send()方法,将其携带的值传递给yield,注意,是传递给yield,而不是x,然后再将其赋值给x;第二,send()方法具有和next()方法一样的功能,也就是说,传值完毕后,会接着上次执行的结果继续执行,直到遇到yield停止。

下例用send和yield实现了一个生产者消费者模型。

 1 def consumer(name):
 2     print('Consumer start...')
 3     while True:
 4         print('[consumer]%s need production'%name)
 5         bone = yield
 6         print('[consumer]%s consumed a production %s'%(name,bone))
 7 
 8 
 9 def producer(obj1):
10     obj1.send(None)
11     for i in range(3):
12         print('[producer] is producing production: %s'%i)
13         obj1.send(i)
14 
15 
16 if __name__ == '__main__':
17     con1 = consumer('ConsumerA')
18     producer(con1)

在3.7以上的python中,则可以用asynio的方式实现协程,实现很像javascript中的await和async

 1 import asyncio
 2 import time
 3 
 4 async def say_after(delay, what):
 5     await asyncio.sleep(delay)
 6     print(what)
 7 
 8 async def main():
 9     print(f"started at {time.strftime('%X')}")
10 
11     await say_after(1, 'hello')
12     await say_after(2, 'world')
13 
14     print(f"finished at {time.strftime('%X')}")
15 
16 asyncio.run(main())

此外,asynio提供了更多协程有关方法来丰富使用,详情参考官方文档。

 

3. Javascript的协程

查阅资料后发现原来Javascript中也有yield关键字。

Javascript中用生成器函数function*实现协程,例子:

 1 function* anotherGenerator(i) {
 2     yield i + 1;
 3     yield i + 2;
 4     yield i + 3;
 5 }
 6 
 7 function* generator(i) {
 8     yield i;
 9     yield* anotherGenerator(i); // 移交执行权
10     yield i + 10;
11 }
12 
13 var gen = generator(10);
14 
15 console.log(gen.next().value); // 10
16 console.log(gen.next().value); // 11
17 console.log(gen.next().value); // 12
18 console.log(gen.next().value); // 13
19 console.log(gen.next().value); // 20

生成器函数的yield关键字有可以交出函数的执行权,挂起自身,然后 JavaScript 引擎去执行这个函数后面的语句,在上面这个例子中,第 15 行调用gen.next()开始执行生成器函数的内容,第一次while循环里yield交出了执行权,JavaScript 引擎转而执行第 16 行,再次调用gen.next(),这时候 JavaScript 接着上次挂起的地方执行。

协程属于ESMC6的标准,而常用的async,await是ESMC7的标准更新,所以还是推荐使用async, await来实现协程,更符合现代JS代码规范。

 

4. C++中的协程

c++直到C++20才引入协程,添加了co_wait, co_yield, co_return关键字来实现

一个简单例子:

 1 #include <chrono>
 2 #include <iostream>
 3 #include <future>
 4 #include <experimental/coroutine>
 5 
 6 using namespace std::chrono_literals;
 7 
 8 std::future<int> foo() {
 9     std::cout << "call foo\n";
10     std::this_thread::sleep_for(3s);
11     co_return 5;
12 }
13 
14 std::future<std::future<int>> bar() {
15     std::cout << "call bar\n";
16     std::cout << "before foo\n";
17     auto n = co_await std::async(foo);
18     std::cout << "after foo\n";
19     co_return n;
20 }
21 
22 int main(int argc, char *argv[])
23 {
24     std::cout << "before bar\n";
25     auto i = bar();
26     std::cout << "after bar\n";
27     i.wait();
28     std::cout << "result = " << i.get().get();
29     return 0;
30 }

(注意带协程的项目要加编译选项 -foroutine)

由于协程在C++20中属于std::experimental的范畴中,并不推荐广泛使用。

 

posted @ 2021-11-09 19:49  Asp1rant  阅读(92)  评论(0编辑  收藏  举报