世界杯海报_u20世界杯德国 - jjswlx.com

使用 c++20 协程与 io
2026-01-10 00:07:36

如果您不熟悉 io_uring 和 c++20 协程,可以参考这个仓库里的其他一些文章和示例代码:

github.com/yunwei37/co-uring-WebServer

这个版本的 echo server 代码由 https://github.com/frevib/io_uring-echo-server 改造而来,是希望通过在 io_uring 的基础上,尝试实现最基本的协程 IO 模式,然后进行性能对比。之前的版本使用了一个 event loop 的模式,并通过 io_uring 的 IORING_OP_PROVIDE_BUFFERS 参数和 IORING_FEAT_FAST_POLL 参数,实现了零拷贝和内核线程的 polling,不需要额外的系统调用开销。

本文在 io_uring-echo-server 的基础上增添了一个简易的协程实现,完整的 demo 代码实现在这里:github.com/yunwei37/co-uring-WebServer/blob/master/demo/io_uring_coroutine_echo_server.cpp

协程实现原先的代码包含一个 event loop,大致是这样(忽略具体细节),进行 IO 和完成 IO 的逻辑是完全分开的:

代码语言:javascript复制 add_accept(....);

// start event loop

while (1) {

io_uring_for_each_cqe(&ring, head, cqe) {

if (type == ACCEPT) {

add_read(....);

add_accept(....);

} else if (type == READ) {

if (read_cout <= 0) {

close();

}

add_write(....);

} else if (type == WRITE) {

add_read(....);

}

}

}这里简单介绍一下协程的实现方式。使用协程的 co_await 关键字,可以让 IO 的异步回调变得更自然,例如对于一个连接进行 echo,我们的协程版本可以写成这样:

代码语言:javascript复制conn_task handle_echo(int fd) {

while (true) {

size_t size_r = co_await echo_read(MAX_MESSAGE_LEN, IOSQE_BUFFER_SELECT);

if (size_r <= 0) {

co_await echo_add_buffer();

shutdown(fd, SHUT_RDWR);

connections.erase(fd);

co_return;

}

co_await echo_write(size_r, 0);

co_await echo_add_buffer();

}

}这里 handle_echo 里面的 read 和 write,和同步的调用编写方式基本一样,只是在前面使用了 co_await 关键字,指明了该函数是个协程。

根据 C++ 的规范,这里的协程是无栈协程,需要实现一个 task 和 promise_type,例如:

代码语言:javascript复制struct conn_task {

struct promise_type

{

using Handle = std::coroutine_handle;

conn_task get_return_object()

{

return conn_task{Handle::from_promise(*this)};

}

std::suspend_always initial_suspend() noexcept {

return {};

}

std::suspend_never final_suspend() noexcept { return {}; }

void return_void() noexcept {}

void unhandled_exception() noexcept {}

struct io_uring *ring;

struct conn_info conn_info;

size_t res;

};

promise_type::Handle handler;

};以 write 为例,它返回一个 awaitable 对象:

代码语言:javascript复制auto echo_write(size_t message_size, unsigned flags) {

struct awaitable {

bool await_ready() { return false; }

void await_suspend(std::coroutine_handle h) {

auto &p = h.promise();

struct io_uring_sqe *sqe = io_uring_get_sqe(p.ring);

io_uring_prep_send(sqe, p.conn_info.fd, &bufs[p.conn_info.bid], message_size, 0);

io_uring_sqe_set_flags(sqe, flags);

p.conn_info.type = WRITE;

memcpy(&sqe->user_data, &p.conn_info, sizeof(conn_info));

}

size_t await_resume() {

return 0;

}

size_t message_size;

unsigned flags;

};

return awaitable{message_size, flags};

}实际上,在运行到 write 调用时,由于 awaitable 对象中的 await_ready 返回 false,协程会在调用 await_suspend 之后停下来,回到主循环,在主循环中,当我们接收到 write 的调用时,只需要简单地通过协程句柄让协程继续运行:

代码语言:javascript复制 ....

if (type == WRITE) {

h.resume();

}

....此时协程会从 await_resume() 中继续运行,并将 await_resume 的返回值作为 write 的返回值。具体细节可以参考仓库中的代码实现。

benchmark运行环境:Linux ubuntu 5.11.0-41-generic #45~20.04.1-Ubuntu SMP Wed Nov 10 10:20:10 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux vmware 16, 8GB ram,Intel Core i7-10750H,2 cores,4 Logical processors;编译指令:g++ io_uring_echo_server.cpp -o ./io_uring_echo_server -Wall -O3 -D_GNU_SOURCE -luring -std=c++2a -fcoroutinesbenchmark tool:https://github.com/haraldh/rust_echo_bench,使用 taskset 将其与一个核心绑定:io_uring with coroutinesource:github.com/yunwei37/co-uring-WebServer/blob/master/demo/io_uring_coroutine_echo_server.cpp

request/sec, 60 sec:

clients

1

50

150

300

500

128 bytes

28635

39206

38985

35658

35013

512 bytes

34693

40981

40536

36040

35251

1000 bytes

22304

46619

43915

35162

34618

io_uring without coroutinesource:github.com/yunwei37/co-uring-WebServer/blob/master/demo/io_uring_echo_server.cpp

request/sec, 60 sec:

clients

1

50

150

300

500

128 bytes

25405

35736

37010

28093

26337

512 bytes

26207

36847

39342

32921

38786

1000 bytes

26077

36865

39312

35115

52847

可能是机器性能的原因,在多线程情况下提升并没有很大。

简单画个图对比一下,可以发现仅仅是简单应用协程的情况下,不仅异步编程模型清晰了不少,性能也获得了一点提升:

测试脚本:

代码语言:javascript复制#!/bin/bash

echo $(uname -a)

if [ "$#" -ne 1 ]; then

echo "Please give port where echo server is running: $0 [port]"

exit

fi

PID=$(lsof -itcp:$1 | sed -n -e 2p | awk '{print $2}')

taskset -cp 0 $PID

for bytes in 1 128 512 1000

do

for connections in 1 50 150 300 500

do

cargo run --release -- --address "localhost:$1" --number $connections --duration 60 --length $bytes

sleep 4

done

donereferencehttps://github.com/frevib/io_uring-echo-serverhttps://git.kernel.dk/cgit/liburing/