漫谈协程(coroutine)

一 什么是协程

协程现在已经不是一个新的技术了,但是由于之前一直在用较低版本的c++,没什么机会使用协程。最近写了不少go的代码,接触到了协程,所以想从零开始学习一下协程。

1. 到底什么是协程

之前听说协程的时候,大家都讲协程就是执行在用户态的微线程,加上go中协程的使用和线程差不多,我也就一直这样理解了。但是真正定义协程的功能是:可以随时的挂起和恢复,它允许多个入口点在不同的执行点挂起和恢复,而不是像线程那样在任何时候都可能被系统强制切换。那么可以随时挂起和恢复到底能解决什么问题呢?下面我们来谈谈协程的优势。

2. 协程的优势

协程拥有轻量,高效,简单等优势。

  1. 轻量:协程一般都是在各个语言的层面上做实现,线程仍然是操作系统运算调度的最小单位,比起线程来,创建协程更加轻量。协程有多种实现方式,当我们在一个线程上分配多个协程时,协程之间就不需要考虑锁机制。
  2. 高效:当我们的线程在执行IO密集型操作时,往往需要等待IO结果,此时操作系统要么做线程的切换,而频繁的切换线程是一个和高额的操作,当使用协程的时候,我们在线程内使用协程将操作挂起,等待IO完成时再继续执行,这样不会发生线程切换等操作。
  3. 简化异步编程:在我们使用rpc框架时,框架往往会提供同步,异步等调用方式,当同步调用其他接口时,当前线程会被阻塞,当异步调用其他接口时,就需要你提供一个回调函数,当有结果返回时,由框架将结果回吐给你。这种编程方式是不方便的,协程可以简化这个操作,后面我们会举例说明。

下面我会介绍协程是如何产生上述优势的,行文逻辑如下,在第二个章节,我会介绍,当已知了协程的功能,使用协程的时候,我们如何简化了异步编程;第三个章节我们会介绍协程是如何实现我们希望的那些功能的。

二 使用协程异步编程

使用异步做网络编程(实现业务逻辑)时,我们的业务代码是有严格的执行顺序的,但是异步的返回是无序的,就使得我们,代码往往需要一些状态码来判断前置调用是否已经完成,如果再叠加了异常处理这些逻辑的话,代码逻辑会非常晦涩难懂,而且容易经常性的形成回调地狱。举个例子,如果我们使用异步回调的方式对一个整型数字做加3的操作,我们有一个加1的函数,加3时需要调用三次:

void AsyncAddOne(int val, std::function callback) {
    std::thread t([value, callback = std::move(callback)] {
        callback(val + 1);
    });
    t.detach();
}

AsyncAddOne(1, [] (int result) {
        AsyncAddOne(result, [] (int result) {
            AsyncAddOne(result, [] (int result) {
                cout << "result is: " << result << endl;
            });
        });
    });

看起来十分的晦涩难懂,现在大部门的服务框架其实已经做了一些优化,比如使用Promise/Future特性。下面只是简单示意一下:

AddOne.then({return AddOne.then({return AddOnde})})

我们拿一个在日常生产过程中的一段实例来示范Promise/Future特性,
示例如下:这段代码的逻辑是使用了两个异步线程分别调用了redis和mysql,拿到结果后做自身的业务处理请求

// 第一个串行任务,CommonTask
trpc::Future CommonHandler() {
  // 1. do something in common handler
  return MakeReadyFuture(res);
}

void HttpHandler() {
  // 1. 处理公共逻辑
  auto http_task = CommonHandler();

  // 2. 任务1完成后,创建并执行并行任务
  auto data_task = http_task.Then([](Future&& result1) {
    // 2.1 创建redis任务,通过redis_proxy发起调用, 并返回相关结果,cmd为请求redis的命令
    trpc::Future fut_redis_task = redis_proxy->AsyncRedis(cmd);

    // 2.2 创建mysql任务, 通过mysql_proxy发起调用, 并返回相关结果,cmd为请求mysql的命令
    trpc::Future fut_mysql_task = mysql_proxy->AsyncMysql(cmd);

    // 将单个任务加入parallel_futs
    parallel_futs.push_back(fut_redis_task);
    parallel_futs.push_back(fut_mysql_task);
    // 若并行任务2.1和2.2都完成了则结束该回调,并进入下一个回调
    auto fut = WhenAll(parallel_futs).Then([](std::vector>&& result2) {
      // 分别获得redis和mysql的result, 进而完成相关任务
      // result[0].GetValue();
      // result[1].GetValue();
      // 3. do something calc handler...

      return trpc::MakeReadyFuture(res);
    });
    return fut;
  });

  // 回包
  data_task.Then([](Future&& result3){
    if (result3.IsReady()) {
      // 4. do something and response to client
      // full succ in reply
    } else {
      // full exception in reply
    }
    SendUnaryResponse(reply);
    // 链式调用最后的then可以返回void
  });
}

虽然Future这种模式已经简化了之前自己写代码判断各个异步任务的完成状态(实际上是封装在了Future自身的逻辑中),但是也有一定的编程复杂度,尤其在涉及到错误处理的时候。
使用协程可以让我们像使用一个线程做同步调用一样,来写我们的一部调用代码。具体是如何做到的,可以参照下文的实现。

三 协程是如何实现的

协程的实现方式有很多种,具体到线程这个点上,有M:N和1:N的实现方式,M:N就是在M个线程上启用N个协程,1:N就是在1个线程上开启N个协程,这两种实现区别也是显而易见的,M:N可以充分利用cpu性能,1:N实现不需要考虑协程间的竞争问题。

我们回顾一下协程需要实现的功能:

  1. 任务挂起
  2. 任务恢复

所以在实现协程时,挂起(co_yield)需要保存当前函数执行的上下文,在恢复执行(co_resume)时需要恢复函数栈帧重新执行。做此类实现一般都需要借助汇编,这里列举几个协程库:https://github.com/Tencent/libco
https://github.com/boostorg/fiber
微信的libco同时也hook了recv等系统调用,在执行网络IO时会自行让渡,在使用时需要加上特殊的链接参数。

后面会对libco做一些分析(未完待续)

你可能感兴趣的:(协程c++)