本文无意于解释什么是协程或它是如何工作的,而是向大家介绍如何在Drogon中使用协程。有很多术语,普通的例程也使用,但是在协程里,意义稍有不同,为了避免引起不必要的混淆,我们列举了一些常用术语。

协程(Coroutine) 是能暂停执行以在之后恢复的函数.
Return 对普通函数来说意味着结束执行并返回一个值。 而协程需要返回一个包含promise_type类型的对象(本文中称作resumable类型),用来恢复这个协程的执行。
(co_)yield意思是协程暂停执行并返回一个值。
co_return意思是协程结束并返回一个值(如果有值的话)。
(co_)await意思是当前的协程正在等待一个结果,如果结果没有立即准备好,比如需要发起网络请求,则当前协程被暂停执行,当前线程将执行其它任务。当结果准备好时,当前协程将被恢复执行(不一定在当前线程恢复)

协程特性在Drogon中是header-only的,这意味着即使构建drogon库的编译器不支持协程,用户也可以 使用协程。如何使能协程和编译器有关,对版本>=10.0的GCC来说,可以通过-std=c++20 -fcoroutines编译参数使能协程。对MSVC来说(MSVC 19.25测试通过)需要设置/std:c++latest并且不能设置/await。例如可以通过如下cmake命令使能drogon的协程(GCC):

注意截至clang12.0, Drogon的协程实现还不能在clang上工作。 而GCC11在c++20标准开启时是默认支持协程的,也就是说,如果编译器是GCC11,则编译Drogon应用程序不需要做任何特别设置。而GCC 10虽然能编译并执行协程,但它有一个编译器bug导致嵌套的协程帧不会被释放,进而导致内存泄漏。

比如,我们想返回数据库中用户的个数:

  1. app.registerHandler("/num_users",
  2. [](HttpRequestPtr req, std::function<void(const HttpResponsePtr&)> callback) -> Task<>
  3. // 返回值必须是某种resumable类型(框架已封装好) ^^^
  4. {
  5. auto sql = app().getDbClient();
  6. try
  7. {
  8. auto result = co_await sql->execSqlCoro("SELECT COUNT(*) FROM users;");
  9. auto resp = HttpResponse::newHttpResponse();
  10. callback(resp);
  11. }
  12. catch(const DrogonDbException &err)
  13. {
  14. // 异常也可以像同步接口那样正常工作
  15. auto resp = HttpResponse::newHttpResponse();
  16. resp->setBody(err.base().what());
  17. callback(resp);
  18. }
  19. co_return; // 该语句不是必须的,因为它位于协程的结束处。因为返回值是Task<void>类型,这里不需要返回任何值
  20. }

几个重要的需要注意的地方:

  1. 任何使用了co_await的handler方法,它自身就成为一个协程,它的返回值就不能是void类型了,必须更换成框架封装好的Task模板。
  2. 普通函数中的return在协程中必须换成co_return
  3. 协程的参数要用值传递。不能是引用。

Task模板遵循了c++ coroutine标准,用户不要太关心它的细节,只需要知道如果希望协程生成T类型的结果,那么返回的类型就是Task<T>

通过值传递参数是协程作为异步执行的一个约束,编译器会自动值拷贝(或者move)这些参数到协程帧上,以便协程恢复时可以正常使用,对于引用参数,协程帧只拷贝它的引用(地址),所以除非确知该参数的生命周期在整个协程执行的期间都有效,请使用值类型作为参数类型。

目前websocket控制器还不支持协程,如果您有需求,请在github上发issue。

常见缺陷

在使用协程时,您可能会遇到一些常见的陷阱。

Lambda 捕获和协程具有不同且独立的生命周期。协程会一直存在直到协程帧被破坏。但匿名 lambda 通常在调用后立即销毁。因此,由于协程的异步性质,协程的 leftime 可能比 lambda 长得多。例如在下面 SQL 的执行中。 lambda 在开始等待 SQL 完成后立即销毁(返回到事件循环以处理其他事件)。而协程帧在等待 SQL。导致当 SQL 刚完成时,lambda 捕获早就被破坏。

  1. auto db = app().getDbClient();
  2. // The lambda object, thus captures destruct right at awaiting. They are destructed at this point
  3. LOG_INFO << "Remove old customers that have no activity for more than " << num << "days"; // use-after-free
  4. });
  5. // BAD, This will crash

Drogon 提供了 async_func 来包裹 lambda 以确保它的生命周期

  1. void removeCustomers(const std::string& customer_id)
  2. {
  3. async_run([&customer_id] {
  4. // ^^^^ DO NOT pass/capture objects by reference into a coroutine
  5. // Unless you are sure the object has a longer lifetime than the coroutine
  6. auto db = app().getDbClient();
  7. co_await db->execSqlCoro("DELETE FROM customers WHERE customer_id = $1", customer_id);
  8. // `customer_id` goes out of scope right at awaiting SQL. Crashes here
  9. co_await db->execSqlCoro("DELETE FROM orders WHERE customer_id = $1", customer_id);
  10. }

但是,将来自协程的对象作为引用传递是可以的