Skip to content

killtimer0/tinycoro

Repository files navigation

tinycoro

Linux下基于io_uring的C++20协程库:提供 task / generator 等基础协程抽象,以及调度器scheduler。支持常用的异步I/O awaitable(read/write/accept/accept_multishot/connect/sleep/yield 等)。代码以头文件接口为主:位于 include/tinycoro/*

整体架构

整体架构

特性概览

  • C++20 协程抽象
    • tinycoro::task<R>:惰性执行(需要 start() 或被 co_await 驱动)、获取返回值、异常传播。
    • tinycoro::generator<T, X, R>:支持 co_yield 迭代,使用send传值(Python like),并在结束后获取返回值。
  • io_uring 调度器
    • tinycoro::schedulerschedule(task) 以及 process_events() 处理事件。
    • 提供 yield()sleep(duration) 等 awaitable。
  • 常用 I/O awaitable:见 async_operations.hpp
  • Promise 自定义分配器:支持通过 std::allocator_arg_t 注入自定义 allocator 来分配协程帧,便于对大量短生命周期协程进行内存池/PMR 优化。
  • 可选 libcurl 集成:检测到 libcurl 时编译 curl_manager及相关测试代码。

依赖与环境

  • Linux(依赖 io_uring,内核版本需要大于5.10)
  • CMake >= 3.10
  • C++20 编译器
  • pkg-config
  • 必需依赖:liburing(开发包)
  • 可选依赖:libcurl(开发包;用于 curl_manager、curl 测试)
  • 可选依赖:asio(开发包;用于 asio 相关基准测试)
# Ubuntu
sudo apt-get update
sudo apt-get install -y cmake g++ pkg-config liburing-dev
# libcurl
sudo apt-get install -y libcurl4-openssl-dev
# asio
sudo apt-get install -y libasio-dev

构建

提供了 CMake presets(见 CMakePresets.json)和 VSCode settings(见 .vscode/*.template.json),也支持普通 CMake 命令。

方式 A:使用 presets(推荐)

cmake --preset native-release
cmake --build build/native-release -j

Debug(带 ASAN/UBSAN/coverage):

cmake --preset native-debug
cmake --build build/native-debug -j

方式 B:手动指定构建目录

cmake -S . -B build/release \
  -DCMAKE_BUILD_TYPE=Release \
  -DTINYCORO_BUILD_TESTS=ON \
  -DTINYCORO_BUILD_BENCHMARKS=ON
cmake --build build/release -j

运行测试

构建完成后:

ctest --test-dir build/native-release --output-on-failure
# 或者
./build/native-release/tests/tinycoro-tests

如果检测到 libcurl,会额外启用 test_curl.cpp 里的集成测试。

运行 Benchmarks

./build/native-release/benchmarks/tinycoro-bench --benchmark_filter=".*"

Benchmark结果

测试环境为Ubuntu24.04,16核 4602MHz x64 CPU

BM_SchedulerNop

测试空操作(nop)的调度性能。

  • Normal:15.1M/s
  • SQPoll:24.0M/s

BM_PingServer_QPS

测试TCP Ping Server的性能。

Redis

使用redis-benchmark -t ping -P 1 -c 500 -n 100000 -q测试,约 156.2k req/s,p50 ≈ 1.671 ms

tinycoro

分别测试基于asio(epoll) / asio(io_uring) / tinycoro(io_uring) / tinycoro(io_uring, SQPoll)的PingPong Server(见bm_tcp_server.cpp),结果如下:

固定连接数为500、总请求数为100000(连接池模式),调整accept线程数量和客户端数量,测试结果如下:

threads_test

固定连接数为500、总请求数为100000、线程数为8,调整每次请求包含的消息数量(pipeline),测试结果如下:

batch_size_test

Quick Start

1) task:惰性执行 + 返回值 + 异常传播

#include <tinycoro/task.hpp>

tinycoro::task<int> compute() {
    co_return 42;
}

int main() {
    auto t = compute();
    t.start();
    return t.result();
}

2) scheduler:调度两个协程并通过 yield() 让出执行权

#include <tinycoro/scheduler.hpp>
#include <tinycoro/task.hpp>

tinycoro::task<> pingpong(tinycoro::scheduler& sched, int& counter) {
    for (int i = 0; i < 100; ++i) {
        ++counter;
        co_await sched.yield();
    }
}

int main() {
    tinycoro::scheduler sched(256);
    int a = 0, b = 0;

    sched.schedule(pingpong(sched, a));
    sched.schedule(pingpong(sched, b));
    sched.process_events();
}

3) generator:像容器一样迭代

#include <tinycoro/generator.hpp>

tinycoro::generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        int next = a + b;
        a = b;
        b = next;
    }
}

int main() {
    int i = 0;
    for (int v : fibonacci()) {
        printf("%d\n", v);
        if (++i == 10) break;
    }
}

4) io_uring I/O awaitable:operations::read / operations::write

接口位于:include/tinycoro/async_operations.hpp

    tinycoro::file read_fd, write_fd;
    make_pipe(read_fd, write_fd);

    tinycoro::scheduler sched(256);
    auto expected_data = "Hello, tinycoro!"sv;
    auto read_task = [&]() -> tinycoro::task<> {
        tinycoro::file readfd = std::move(read_fd);
        size_t offset = 0;
        std::vector<char> buffer(expected_data.size());
        while (offset < expected_data.size()) {
            int res = co_await ops::read(
                sched, readfd, std::span<char>(buffer.data() + offset, buffer.size() - offset));
            offset += res;
        }
        int res = co_await ops::read(
            sched, readfd, std::span<char>(buffer.data(), 1));
        EXPECT_EQ(res, 0);
        EXPECT_EQ(std::string_view(buffer.data(), buffer.size()), expected_data);
    };
    auto write_task = [&]() -> tinycoro::task<> {
        tinycoro::file writefd = std::move(write_fd);
        for (auto c : expected_data) {
            int res = co_await ops::write(sched, writefd, std::span<char>(&c, 1));
            EXPECT_EQ(res, 1);
        }
    };

    sched.schedule(read_task());
    sched.schedule(write_task());
    sched.process_events();

完整例子见测试文件:test_scheduler.cpp

5) 可选:libcurl 集成

#include <tinycoro/curl_manager.hpp>
#include <tinycoro/task.hpp>

namespace curl = tinycoro::curl;

tinycoro::task<> fetch(tinycoro::scheduler& sched) {
    curl::manager manager(sched, 10);

    curl::handle_t handle;
    curl_easy_setopt(handle, CURLOPT_URL, "http://www.example.com/");
    curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L);

    CURLcode ret = co_await manager.perform(handle);
    // use the handle
}

对应测试在:test_curl.cpp

Promise 自定义分配器(promise_allocator

tinycorotaskgenerator 都在其 promise_type 上继承了 tinycoro::promise_allocator,用于自定义协程帧的分配与释放

协程在创建时会分配一块“协程帧”(包含 promise、局部变量、await 状态等)。在高并发/大量短协程场景下,这部分分配可能成为热点。promise_allocator 提供了 operator new/delete

  • 默认路径:未传 allocator 时,选择普通 ::operator new
  • 注入 allocator:当协程函数参数列表匹配 std::allocator_arg_t, Alloc, ...时,编译器会优先选择对应的 operator new 重载,从而使用自定义 allocator 分配协程帧。

下面的示例用 std::pmr 的内存资源给协程帧做池化(也可以使用任意符合标准的 allocator):

#include <memory_resource>
#include <tinycoro/task.hpp>

tinycoro::task<int> compute(
    std::allocator_arg_t,
    std::pmr::polymorphic_allocator<>,
    int x)
{
    co_return x + 1;
}

int main() {
    std::pmr::monotonic_buffer_resource pool;
    std::pmr::polymorphic_allocator<> alloc(&pool);

    {
        // by function
        auto t = compute(std::allocator_arg, alloc, 41);
        t.start();
        assert(t.result() == 42);
    }

    {
        // by lambda
        auto work = [n = 1](std::allocator_arg_t, std::pmr::polymorphic_allocator<>) -> tinycoro::task<int> {
            co_return n;
        };
        auto t = work(std::allocator_arg, alloc);
        t.start();
        assert(t.result() == 1);
    }
}

注意事项

  • allocator 相关对象(例如 std::pmr::memory_resource)的生命周期需要长于协程对象的生命周期(例如 generator/task 的生命周期,直到对应的 coroutine handle 被 destroy())为止。
  • 该机制只影响协程帧的分配,不会自动替换协程体内对容器/缓冲区的分配;后者仍需显式使用 PMR 容器或自定义 allocator。

CMake 选项

  • TINYCORO_BUILD_TESTS:构建单测(默认 ON)
  • TINYCORO_BUILD_BENCHMARKS:构建 benchmarks(默认 OFF)
  • TINYCORO_CURRENT_LOGLEVEL:设置日志级别
  • TINYCORO_ENABLE_ASAN / TINYCORO_ENABLE_UBSAN / TINYCORO_ENABLE_COVERAGE

About

A simple coroutine library based on C++20 coroutine and liburing

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors