乐趣区

关于协程:漫谈协程coroutine

一 什么是协程

协程当初曾经不是一个新的技术了,然而因为之前始终在用较低版本的 c ++,没什么机会应用协程。最近写了不少 go 的代码,接触到了协程,所以想从零开始学习一下协程。

1. 到底什么是协程

之前据说协程的时候,大家都讲协程就是执行在用户态的微线程,加上 go 中协程的应用和线程差不多,我也就始终这样了解了。然而真正定义协程的性能是:能够随时的挂起和复原,它容许多个入口点在不同的执行点挂起和复原,而不是像线程那样在任何时候都可能被零碎强制切换。那么能够随时挂起和复原到底能解决什么问题呢?上面咱们来谈谈协程的劣势。

2. 协程的劣势

协程领有轻量,高效,简略等劣势。

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

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

二 应用协程异步编程

应用异步做网络编程 (实现业务逻辑) 时,咱们的业务代码是有严格的执行程序的,然而异步的返回是无序的,就使得咱们,代码往往须要一些状态码来判断前置调用是否曾经实现,如果再叠加了异样解决这些逻辑的话,代码逻辑会十分艰涩难懂,而且容易经常性的造成回调天堂。举个例子,如果咱们应用异步回调的形式对一个整型数字做加 3 的操作,咱们有一个加 1 的函数,加 3 时须要调用三次:

void AsyncAddOne(int val, std::function<void (int)> 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<Result> CommonHandler() {
  // 1. do something in common handler
  return MakeReadyFuture<Result1>(res);
}

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

  // 2. 工作 1 实现后,创立并执行并行任务
  auto data_task = http_task.Then([](Future<Result1>&& result1) {
    // 2.1 创立 redis 工作,通过 redis_proxy 发动调用, 并返回相干后果,cmd 为申请 redis 的命令
    trpc::Future<Result2> fut_redis_task = redis_proxy->AsyncRedis(cmd);

    // 2.2 创立 mysql 工作, 通过 mysql_proxy 发动调用, 并返回相干后果,cmd 为申请 mysql 的命令
    trpc::Future<Result2> 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<Future<Result2>>&& result2) {
      // 别离取得 redis 和 mysql 的 result, 进而实现相干工作
      // result[0].GetValue();
      // result[1].GetValue();
      // 3. do something calc handler...

      return trpc::MakeReadyFuture<Resul3t>(res);
    });
    return fut;
  });

  // 回包
  data_task.Then([](Future<Result3>&& 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 做一些剖析(未完待续)

退出移动版