开源软件

光速上手C++20协程

现代C++经过多年发展,早已变得非常易用。从C++11、C++14到C++17,各种优秀特性不断纳入。2020年C++20标准一发布,大家就躁动起来了。C++20协程正式登场。今天就来带大家高速上手C++20协程

概述

协程分有栈协程和无栈协程两种。今天来看看怎么用C++20无栈协程。C++20协程根本不能给用户直接拿来做应用开发,因为它是面向C++库作者的。非常裸露,直接拿开开发业务会炒鸡难用。

今天来学习下怎么使用基于C++20设计开发的协程框架async_simpleasync_simple是阿里巴巴开源的轻量级C++异步框架。提供了基于C++20无栈协程(Lazy),有栈协程(Uthread)以及Future/Promise等异步组件。连续两年经历天猫双十一磨砺,承担了亿级别流量洪峰,具备高性能和高稳定性。

小栗子

协程最大好处在于可以把同步代码无缝变成异步运行代码。不用像回调函数那样把业务逻辑分开来写,提高异步系统代码可读性和可维护性。下面这个栗子展示了同步读文件。

int bar() {
    // ...
    int r = read_some();
    // ...
    return r;
}

int read_some() {
    // ...
    return read(); // syscall
    // ...
}

这个片段代码展示传统同步阻塞代码逻辑。业务上层函数逐层往下同步调用,一直走到同步系统调用。然后同步阻塞陷入内核态。这种模式开发的系统一般吞吐能力低。然后大家会开始考虑做异步化改造。传统异步化手段使用回调函数做。如下面这个栗子。

template <class Callback>
void bar(Callback&& cb) {
  // ...
  read_some([cb = std::move(cb)](int r) {
    // ...
    cb(r);
  });
}

template <class Callback>
void read_some(Callback&& cb) {
  // ...
  submit_io([cb = std::move(cb)](int r) {
    // ...
    cb(r);
  });
}

template <class Callback>
void submit_io(Callback&& cb) {
  // libaio/epoll
}

一般用Linux AIO实现异步文件IO,使用Epoll实现网络IO异步非阻塞访问。大家会基于AIO/Epoll封装对应异步IO提交API。这些API接受一个回调函数,IO结束后,回调函数被调用。上层在使用这些回调API开发时,很容易写出上面那种代码。形成回调地狱。代码可读性很差。有了async_simple协程框架后,我们这样写代码。

template <class T>
using Lazy = async_simple::coro::Lazy<T>;

Lazy<int> bar() {
  // ...
  int r = co_await read_some();
  // ...
  co_return r;
}

Lazy<int> read_some() {
  // ...
  int r = co_await read_coro();
  // ...
  co_return r;
}

async_simple::Future<int> read_coro() {
  Promise<int> p;
  auto fut = p.getFuture();
  submit_io([p = std::move(p)](int r) {
    // ...
    p.setValue(r);
  });
  return fut;
}

很简单,写起来和Python/Js协程类似。我们把C++普通函数返回值T改成async_simple::coro::Lazy<T>类型,并把return都改成co_return后,这个普通函数就变成了C++20协程函数。C++20引入了co_await关键字来在协程函数中调用其他协程函数。在协程函数中调用普通C++函数还是保持和原来一样,不用加co_await

协程函数一路调到底,还是会走到最底层回调API。async_simple提供了Future/Promise组件来让协程对接回调函数。返回Future<T>类型的普通C++函数可以被co_await调用。

Future是一种值在未来被满足的对象。由对应Promise对象在未来一个时刻设置上Future的值。在对接回调函数时,构造一对Future/Promise,将Promise传递给底层IO所需要的回调函数中,把Future直接返回。上层函数co_await Future对象时,当Future值未满足,当前协程被自动挂起。当IO结束后回调执行,Promise设置值。之前被挂起的协程将会被恢复执行。

可以看到,使用async_simple进行异步化改造时,只需要把之前同步代码改下返回值类型,改下co_await/co_return即可。代码依然是同步阻塞风格编写,运行却是异步执行。

更多栗子

async_simple样例展示中,有很多使用栗子。例如基于asio提供异步网络访问接口开发的async_echo_server代码如下。可以看到依然很简单。

using asio::ip::tcp;

async_simple::coro::Lazy<void> session(tcp::socket sock) {
  int msg_index = 0;
  for (;;) {
    const size_t max_length = 1024;
    char data[max_length];
    auto [error, length] =
      co_await async_read_some(sock, asio::buffer(data, max_length));
    msg_index++;
    if (error == asio::error::eof) {
      std::cout << "Remote client closed at message index: "
                << msg_index - 1 << ".\n";
      break;
    } else if (error) {
      std::cout << error.message() << '\n';
      throw asio::system_error(error);
    }
    co_await async_write(sock, asio::buffer(data, length));
  }
  std::error_code ec;
  sock.shutdown(asio::ip::tcp::socket::shutdown_both, ec);
  sock.close(ec);
  std::cout << "Finished echo message, total: " << msg_index - 1 << ".\n";
}

async_simple::coro::Lazy<void> start_server(asio::io_context& io_context,
                                            unsigned short port,
                                            async_simple::Executor* E) {
  tcp::acceptor a(io_context, tcp::endpoint(tcp::v4(), port));
  std::cout << "Listen port " << port << " successfully.\n";
  for (;;) {
    tcp::socket socket(io_context);
    auto error = co_await async_accept(a, socket);
    if (error) {
      std::cout << "Accept failed, error: " << error.message() << '\n';
      continue;
    }
    std::cout << "New client comming.\n";
    session(std::move(socket)).via(E).detach();
  }
}

int main(int argc, char* argv[]) {
  try {
    asio::io_context io_context;
    std::thread thd([&io_context] {
        asio::io_context::work work(io_context);
        io_context.run();
    });
    AsioExecutor executor(io_context);
    async_simple::coro::syncAwait(start_server(io_context, 9980, &executor));
    thd.join();
  } catch (std::exception& e) {
    std::cerr << "Exception: " << e.what() << "\n";
  }
  return 0;
}

总结

C++20协程对于库开发者来说其实挺复杂。各种awaitable、awaiter、promise_type/co_await等概念。但于C++20用户而言根本不用关心这些东西。只要会用co_await关键字和协程框架提供的协程组件即可。复杂性都交给库作者吧。

async_simple目前有llvm/clang开发者兼C++标准委员会成员参与开发。国内率先大规模应用于生产环境的C++20协程异步框架。如果你觉得async_simple不错,欢迎前往贡献,提Issue,点赞Star!

传送门:https://github.com/alibaba/async_simple

如果想详细了解C++20协程原理,推荐前往:https://lewissbaker.github.io

对这篇文章感觉如何?

太棒了
0
不错
0
爱死了
2
不太好
0
感觉很糟
0
雨落清风。心向阳

    You may also like

    Leave a reply

    您的电子邮箱地址不会被公开。 必填项已用 * 标注

    此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

    More in:开源软件

    开源软件

    嵌入式 Linux 的瑞士军刀:BusyBox

    上期文章中,笔者向各位介绍了 musl,一个 Glibc 的替代方案,它的主要优势体现在更加整洁的代码、更小的二进制体积和更优秀的静态链接支持。而本期文章的主角:BusyBox 则是 GNU Core […]
    开源项目

    Pwnagotchi 开箱教程

    Pwnagotchi 是一个由 Bettercap 驱动的 A2C 的 “AI”,它能够从周围的 WiFi 环境中学习,以最大限度地利用它捕获的可破解 WPA 密钥材料,该材料将作为可被 hashcat 破解的 PCAP 文件收集在磁盘上。 简单来说,Pwnagotchi ...
    开源软件

    在 Linux 终端中管理您的密码

    在信息时代,我们的所使用的密码只会越来越多,你是否遇到过密码太多而经常遗忘?所以应该如何高效地管理这些密码?本篇文章主要讲述了在我们拥有大量的密码时,在Linux终端下,使用 Pass 管理系统高效地管理密码,并为我们提供了详细的操作步骤,使我们更加快速熟悉地掌握如何使用 Pass,减少我们自己在以后的使用中的可能遇到的烦恼。
    开源软件

    用 Scribus 来进行排版吧!

    想不想制作一些亲手设计的小册子呢?来使用专业级的开源软件 Scribus 快捷排版吧!这款应用可以让你轻松地把创意落地成可打印的文档,无论在家、办公室还是专业的印刷厂都可以使用。在过程中也不会用到任何剪刀、胶水,完美契合不善动手的人。