開源軟體

光速上手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
不錯
1
愛死了
2
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:開源軟體

    開源軟體

    用 Scribus 來進行排版吧!

    想不想製作一些親手設計的小冊子呢?來使用專業級的開源軟體 Scribus 快捷排版吧!這款應用可以讓你輕鬆地把創意落地成可列印的文檔,無論在家、辦公室還是專業的印刷廠都可以使用。在過程中也不會用到任何剪刀、膠水,完美契合不善動手的人。
    開源軟體

    使用 pdftk-java 來編輯 PDF 吧!

    在用命令行處理大量 PDF 文件時,手動操作通常不是一個好的選擇。這時候可以選擇使用 pdftk-java 工具,配合上對應批量操作的 Makefile 文件,能讓你事半功倍。
    開源軟體

    來點更高雅的!用 Linux Sampler 演奏數字管弦樂

    一直以來,音樂合成器都在嘗試模擬真實的樂器。然而在技術的發展中,合成音樂家們發現如果想真正地捕獲到樂器的美感,只能去錄製他們的聲音,再通過技術合成手段來獲得想要的音樂。如果你想要為自己的遊戲或者其他應用來配上一段優美的音樂,聘請管弦樂隊顯然是非常昂貴的。但通過 Linux Sampler 這個開源程序,或者再加上一個 MIDI 鍵盤,也是可以實現一段成功的演奏的。
    開源軟體

    來點節奏感吧!在 Linux 上用 Hydrogen 敲鼓

    藉助現代科技,音樂不需要從樂器演奏出來,通過電子合成器就可以創作出優美的音樂。Hydrogen 應用是一個功能齊全並且開源的鼓音樂合成器,並且使用簡單,無論新手還是專業人士都能夠很快上手。