├── benchmarks ├── pong_asio.is_not_compiled_with_makefile ├── pong_asio.please_use_xmake_or_manually_compile ├── pong_asio.cpp ├── pingpong.py ├── README ├── pong.cpp ├── ping.cpp └── ping_when_any.cpp ├── include ├── uring_exec │ ├── io_uring_exec.h │ ├── underlying_io_uring.h │ ├── utils.h │ ├── detail.h │ ├── io_uring_exec_operation.h │ ├── io_uring_exec_sender.h │ ├── io_uring_exec_internal.h │ └── io_uring_exec_internal_run.h └── uring_exec.hpp ├── .gitignore ├── Makefile ├── tests ├── get_scheduler.cpp └── read_env.cpp ├── examples ├── per_operation_cancellation.cpp ├── hello_coro.cpp ├── stop_token.cpp ├── thread_pool.cpp ├── timer.cpp ├── hello_world.cpp ├── signal_handling.cpp └── echo_sender.cpp ├── xmake.lua ├── LICENSE └── README.md /benchmarks/pong_asio.is_not_compiled_with_makefile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /benchmarks/pong_asio.please_use_xmake_or_manually_compile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /include/uring_exec/io_uring_exec.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "io_uring_exec_internal.h" 3 | namespace uring_exec { 4 | 5 | using io_uring_exec = internal::io_uring_exec; 6 | 7 | } // namespace uring_exec 8 | -------------------------------------------------------------------------------- /include/uring_exec.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Core implementation. 4 | #include "uring_exec/io_uring_exec.h" 5 | 6 | // Public APIs (async_* senders). 7 | #include "uring_exec/io_uring_exec_sender.h" 8 | 9 | // Learn how to extend the uring_exec. 10 | #include "uring_exec/io_uring_exec_operation.h" 11 | 12 | // Not so important things. 13 | #include "uring_exec/utils.h" 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # VsCode 35 | .vscode 36 | 37 | # Build files 38 | build 39 | .xmake 40 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean 2 | 3 | INCLUDE = include 4 | EXAMPLES = examples 5 | BENCH = benchmarks 6 | BUILD = build 7 | 8 | # Will not compile. 9 | NEED_EXTERNAL_DEPENDENCY = $(BENCH)/pong_asio.cpp 10 | 11 | # NEED_EXTERNAL_DEPENDENCY is not included. 12 | ALL_EXAMPLES_TARGETS = $(notdir $(basename $(wildcard $(EXAMPLES)/*.cpp))) 13 | ALL_BENCH_TARGETS = $(notdir $(basename $(filter-out $(NEED_EXTERNAL_DEPENDENCY), $(wildcard $(BENCH)/*.cpp)))) 14 | 15 | CXX_FLAGS = -std=c++20 -Wall -Wextra -g -I$(INCLUDE) $^ -luring -pthread 16 | CXX_FLAGS_DEBUG = 17 | 18 | all: examples benchmarks 19 | 20 | examples: $(ALL_EXAMPLES_TARGETS) 21 | 22 | benchmarks: $(ALL_BENCH_TARGETS) 23 | 24 | benchmark_script: 25 | python $(BENCH)/pingpong.py 26 | 27 | clean: 28 | @rm -rf $(BUILD) 29 | 30 | %: $(EXAMPLES)/%.cpp 31 | @mkdir -p $(BUILD) 32 | $(CXX) $(CXX_FLAGS) $(CXX_FLAGS_DEBUG) -o $(BUILD)/$@ 33 | 34 | %: $(BENCH)/%.cpp 35 | @mkdir -p $(BUILD) 36 | $(CXX) $(CXX_FLAGS) $(CXX_FLAGS_DEBUG) -O3 -o $(BUILD)/$@ 37 | -------------------------------------------------------------------------------- /tests/get_scheduler.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "uring_exec.hpp" 5 | 6 | using uring_exec::io_uring_exec; 7 | 8 | int main() { 9 | io_uring_exec uring(512); 10 | auto where = [](std::string_view prefix) { 11 | auto id = std::this_thread::get_id(); 12 | std::cout << prefix << ":\t" 13 | << id 14 | << std::endl; 15 | return id; 16 | }; 17 | auto main_id = where("#main_thread"); 18 | std::jthread j([&](auto token) { 19 | where("#context"); 20 | uring.run(token); 21 | }); 22 | 23 | stdexec::sender auto s = 24 | stdexec::starts_on(uring.get_scheduler(), stdexec::just()) 25 | | stdexec::let_value([where] { 26 | return 27 | stdexec::get_scheduler() 28 | | stdexec::let_value([where](auto scheduler) { 29 | return stdexec::starts_on(scheduler, stdexec::just(where("#nested"))); 30 | }); 31 | }); 32 | auto [id] = stdexec::sync_wait(std::move(s)).value(); 33 | assert(j.get_id() == id); 34 | assert(main_id != id); 35 | } 36 | -------------------------------------------------------------------------------- /tests/read_env.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "uring_exec.hpp" 5 | 6 | using uring_exec::io_uring_exec; 7 | 8 | int main() { 9 | io_uring_exec uring(512); 10 | auto where = [](std::string_view prefix) { 11 | auto id = std::this_thread::get_id(); 12 | std::cout << prefix << ":\t" 13 | << id 14 | << std::endl; 15 | return id; 16 | }; 17 | auto main_id = where("#main_thread"); 18 | std::jthread j([&](auto token) { 19 | where("#context"); 20 | uring.run(token); 21 | }); 22 | 23 | stdexec::sender auto s = 24 | stdexec::starts_on(uring.get_scheduler(), stdexec::just()) 25 | | stdexec::let_value([where] { 26 | return 27 | stdexec::read_env(stdexec::get_scheduler) 28 | | stdexec::let_value([where](auto scheduler) { 29 | return stdexec::starts_on(scheduler, stdexec::just(where("#nested"))); 30 | }); 31 | }); 32 | auto [id] = stdexec::sync_wait(std::move(s)).value(); 33 | assert(j.get_id() == id); 34 | assert(main_id != id); 35 | } 36 | -------------------------------------------------------------------------------- /examples/per_operation_cancellation.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "uring_exec.hpp" 5 | 6 | using uring_exec::io_uring_exec; 7 | 8 | auto make_sender(io_uring_exec::scheduler scheduler, 9 | std::chrono::steady_clock::duration duration) 10 | { 11 | return 12 | stdexec::schedule(scheduler) 13 | | stdexec::let_value([=] { 14 | auto sout = std::osyncstream{std::cout}; 15 | sout << duration.count() << " is on thread:" 16 | << std::this_thread::get_id() << std::endl; 17 | return uring_exec::async_wait(scheduler, duration); 18 | }); 19 | } 20 | 21 | int main() { 22 | io_uring_exec uring({.uring_entries = 8}); 23 | std::array threads; 24 | for(auto &&j : threads) { 25 | j = std::jthread([&](auto token) { uring.run(token); }); 26 | } 27 | using namespace std::chrono_literals; 28 | std::cout << "#main is on thread: " 29 | << std::this_thread::get_id() << std::endl; 30 | stdexec::scheduler auto s = uring.get_scheduler(); 31 | stdexec::sender auto _3s = make_sender(s, 3s); 32 | stdexec::sender auto _9s = make_sender(s, 9s); 33 | // Waiting for 3 seconds, not 9 seconds. 34 | stdexec::sender auto any = exec::when_any(std::move(_3s), std::move(_9s)); 35 | stdexec::sync_wait(std::move(any)); 36 | std::cout << "bye" << std::endl; 37 | } 38 | -------------------------------------------------------------------------------- /examples/hello_coro.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "uring_exec.hpp" 7 | 8 | using uring_exec::io_uring_exec; 9 | using namespace std::chrono_literals; 10 | 11 | int main() { 12 | io_uring_exec uring(512); 13 | stdexec::scheduler auto scheduler = uring.get_scheduler(); 14 | 15 | auto println = [](auto &&...args) { 16 | (std::cout << ... << args) << std::endl; 17 | }; 18 | auto where = [=](const char *flag) { 19 | println(flag, "\tYou are here: ", std::this_thread::get_id()); 20 | }; 21 | 22 | where("(#main thread)"); 23 | 24 | std::jthread j {[&](std::stop_token stop_token) { 25 | where("(#jthread)"); 26 | uring.run(stop_token); 27 | }}; 28 | 29 | std::this_thread::sleep_for(2s); 30 | 31 | auto next = [=] { println(); }; 32 | auto line = [=] { println("===================="); }; 33 | auto reader_friendly = [=] { next(), line(), next(); }; 34 | 35 | reader_friendly(); 36 | 37 | auto [n] = stdexec::sync_wait(std::invoke( 38 | [=](auto scheduler) -> exec::task { 39 | where("(#before...)"); 40 | co_await exec::reschedule_coroutine_on(scheduler); 41 | where("(#after...)"); 42 | 43 | next(); 44 | 45 | println("hello stdexec! and ..."); 46 | co_await uring_exec::async_wait(scheduler, 2s); 47 | 48 | std::string_view hi = "hello coroutine!\n"; 49 | stdexec::sender auto s = 50 | uring_exec::async_write(scheduler, STDOUT_FILENO, hi.data(), hi.size()); 51 | co_return co_await std::move(s); 52 | }, scheduler) 53 | ).value(); 54 | 55 | reader_friendly(); 56 | println("written bytes: ", n); 57 | where("(#goodbye)"); 58 | } 59 | -------------------------------------------------------------------------------- /include/uring_exec/underlying_io_uring.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "detail.h" 4 | namespace uring_exec { 5 | 6 | // Is-a immovable io_uring. 7 | struct underlying_io_uring: public detail::immovable, 8 | public ::io_uring 9 | { 10 | struct constructor_parameters { 11 | unsigned uring_entries; 12 | int uring_flags = 0; 13 | bool sticky = false; // Use the provided flags exactly as-is. 14 | int ring_fd = -1; // Shared work queues. 15 | }; 16 | 17 | // Example: io_uring_exec uring({.uring_entries=512}); 18 | underlying_io_uring(constructor_parameters p) { 19 | initiate(p); 20 | } 21 | 22 | underlying_io_uring(unsigned uring_entries, int uring_flags = 0) 23 | : underlying_io_uring({.uring_entries = uring_entries, .uring_flags = uring_flags}) 24 | {} 25 | 26 | ~underlying_io_uring() { 27 | io_uring_queue_exit(this); 28 | } 29 | 30 | private: 31 | void initiate(constructor_parameters params) { 32 | io_uring_params underlying_params {}; 33 | auto as_is = params.uring_flags; 34 | if(!params.sticky) { 35 | as_is = as_is 36 | // We're using thread-local urings. 37 | | IORING_SETUP_SINGLE_ISSUER 38 | // Improve throughput for server usage. 39 | | IORING_SETUP_COOP_TASKRUN; 40 | } 41 | if(!params.sticky && params.ring_fd > -1) { 42 | as_is = as_is 43 | // Shared backend. 44 | | IORING_SETUP_ATTACH_WQ; 45 | } 46 | underlying_params.flags = as_is; 47 | underlying_params.wq_fd = params.ring_fd; 48 | 49 | if(int err = io_uring_queue_init_params(params.uring_entries, this, &underlying_params)) { 50 | throw std::system_error(-err, std::system_category()); 51 | } 52 | } 53 | }; 54 | 55 | } // namespace uring_exec 56 | -------------------------------------------------------------------------------- /examples/stop_token.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "uring_exec.hpp" 3 | 4 | using uring_exec::io_uring_exec; 5 | 6 | int main() { 7 | // Default behavior: Infinite run(). 8 | // { 9 | // io_uring_exec uring({.uring_entries=8}); 10 | // std::jthread j([&] { uring.run(); }); 11 | // } 12 | 13 | // Per-run() user-defined stop source (external stop token). 14 | auto user_defined = [](auto stop_source) { 15 | io_uring_exec uring({.uring_entries=8}); 16 | auto stoppable_run = [&](auto stop_token) { uring.run(stop_token); }; 17 | std::jthread j(stoppable_run, stop_source.get_token()); 18 | stop_source.request_stop(); 19 | }; 20 | user_defined(std::stop_source {}); 21 | user_defined(stdexec::inplace_stop_source {}); 22 | std::cout << "case 1: stopped." << std::endl; 23 | 24 | // Per-io_uring_exec stop source. 25 | { 26 | using uring_stop_source_type = io_uring_exec::underlying_stop_source_type; 27 | static_assert( 28 | std::is_same_v || 29 | std::is_same_v 30 | ); 31 | io_uring_exec uring({.uring_entries=8}); 32 | std::jthread j([&] { uring.run(); }); 33 | uring.request_stop(); 34 | } 35 | std::cout << "case 2: stopped." << std::endl; 36 | 37 | // Per-std::jthread stop source. 38 | { 39 | io_uring_exec uring({.uring_entries=8}); 40 | std::jthread j([&](std::stop_token token) { uring.run(token); }); 41 | } 42 | std::cout << "case 3: stopped." << std::endl; 43 | 44 | // Heuristic algorithm (autoquit). 45 | { 46 | io_uring_exec uring({.uring_entries=8}); 47 | constexpr auto autoquit_policy = io_uring_exec::run_policy {.autoquit=true}; 48 | std::jthread j([&] { uring.run(); }); 49 | } 50 | std::cout << "case 4: stopped." << std::endl; 51 | } 52 | -------------------------------------------------------------------------------- /examples/thread_pool.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "uring_exec.hpp" 7 | 8 | using uring_exec::io_uring_exec; 9 | 10 | int main() { 11 | io_uring_exec uring({.uring_entries=512}); 12 | stdexec::scheduler auto scheduler = uring.get_scheduler(); 13 | exec::async_scope scope; 14 | 15 | constexpr size_t pool_size = 4; 16 | constexpr size_t user_number = 4; 17 | constexpr size_t some = 10000; 18 | 19 | std::atomic refcnt {}; 20 | 21 | auto thread_pool = std::array{}; 22 | for(auto &&j : thread_pool) { 23 | j = std::jthread([&](auto token) { uring.run(token); }); 24 | } 25 | 26 | auto users = std::array{}; 27 | auto user_request = [&refcnt](int i) { 28 | refcnt.fetch_add(i, std::memory_order::relaxed); 29 | }; 30 | auto user_frequency = std::views::iota(1) | std::views::take(some); 31 | auto user_post_requests = [&] { 32 | for(auto i : user_frequency) { 33 | stdexec::sender auto s = 34 | stdexec::schedule(scheduler) 35 | | stdexec::then([&, i] { user_request(i); }) 36 | | stdexec::let_value([scheduler] { 37 | return 38 | uring_exec::async_nop(scheduler) 39 | | stdexec::then([](...){}); 40 | }); 41 | scope.spawn(std::move(s)); 42 | } 43 | }; 44 | 45 | for(auto &&j : users) j = std::jthread(user_post_requests); 46 | for(auto &&j : users) j.join(); 47 | // Fire but don't forget. 48 | stdexec::sync_wait(scope.on_empty()); 49 | 50 | assert(refcnt == [&](...) { 51 | size_t sum = 0; 52 | for(auto i : user_frequency) sum += i; 53 | return sum * user_number; 54 | } ("Check refcnt value.")); 55 | 56 | std::cout << "done: " << refcnt << std::endl; 57 | } 58 | -------------------------------------------------------------------------------- /examples/timer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "uring_exec.hpp" 6 | 7 | using uring_exec::io_uring_exec; 8 | using namespace std::chrono_literals; 9 | using namespace std::chrono; 10 | 11 | auto global_clock() { 12 | static auto init = steady_clock::now(); 13 | auto now = steady_clock::now(); 14 | auto delta = duration_cast(now - init); 15 | return [=] { std::cout << "global:" << delta << std::endl; }; 16 | }; 17 | 18 | int main() { 19 | io_uring_exec uring(512); 20 | stdexec::scheduler auto scheduler = uring.get_scheduler(); 21 | 22 | std::cout << "start." << std::endl; 23 | global_clock(); 24 | 25 | auto s1 = 26 | stdexec::schedule(scheduler) 27 | | stdexec::let_value([=] { 28 | return uring_exec::async_wait(scheduler, 2s); 29 | }) 30 | | stdexec::then([](...) { 31 | std::cout << "s1:2s" << std::endl; 32 | global_clock()(); 33 | }) 34 | | stdexec::let_value([=] { 35 | return uring_exec::async_wait(scheduler, 2s); 36 | }) 37 | | stdexec::then([](...) { 38 | std::cout << "s1:4s" << std::endl; 39 | global_clock()(); 40 | }); 41 | 42 | auto s2 = 43 | stdexec::schedule(scheduler) 44 | | stdexec::let_value([=] { 45 | return uring_exec::async_wait(scheduler, 1s); 46 | }) 47 | | stdexec::then([](...) { 48 | std::cout << "s2:1s" << std::endl; 49 | global_clock()(); 50 | }) 51 | | stdexec::let_value([=] { 52 | return uring_exec::async_wait(scheduler, 2s); 53 | }) 54 | | stdexec::then([](...) { 55 | std::cout << "s2:3s" << std::endl; 56 | global_clock()(); 57 | }); 58 | std::jthread j([&] { uring.run(); }); 59 | stdexec::sync_wait(stdexec::when_all(std::move(s1), std::move(s2))); 60 | uring.request_stop(); 61 | } 62 | -------------------------------------------------------------------------------- /xmake.lua: -------------------------------------------------------------------------------- 1 | set_project("uring_exec") 2 | set_languages("c++20") 3 | set_targetdir("build") 4 | 5 | -- xmake-repo is outdated. (stdexec is changing too frequently.) 6 | add_requires("stdexec main", { 7 | alias = "stdexec_latest", 8 | system = false, 9 | configs = { repo = "github:NVIDIA/stdexec" } 10 | }) 11 | add_requires("liburing", { 12 | system = false, 13 | configs = { repo = "github:axboe/liburing" } 14 | }) 15 | add_requires("asio", { 16 | system = false, 17 | configs = { repo = "github:chriskohlhoff/asio" } 18 | }) 19 | 20 | add_includedirs("include") 21 | add_cxxflags("-Wall", "-Wextra", "-g") 22 | add_links("uring", "pthread") 23 | 24 | local examples_targets = {} 25 | local benchmarks_targets = {} 26 | 27 | -- For example, xmake build timer 28 | for _, file in ipairs(os.files("examples/*.cpp")) do 29 | local name = path.basename(file) 30 | target(name) 31 | set_kind("binary") 32 | add_files(file) 33 | add_packages("stdexec_latest", "liburing") 34 | table.insert(examples_targets, name) 35 | end 36 | 37 | -- For example, xmake build ping 38 | for _, file in ipairs(os.files("benchmarks/*.cpp")) do 39 | local name = path.basename(file) 40 | target(name) 41 | set_kind("binary") 42 | add_files(file) 43 | add_cxxflags("-DASIO_HAS_IO_URING -DASIO_DISABLE_EPOLL") 44 | add_cxxflags("-O3") 45 | add_packages("stdexec_latest", "liburing", "asio") 46 | table.insert(benchmarks_targets, name) 47 | end 48 | 49 | target("examples") 50 | set_kind("phony") 51 | add_deps(table.unpack(examples_targets)) 52 | 53 | -- Usage: xmake run benchmarks --server [uring_exec|asio] 54 | target("benchmarks") 55 | set_kind("phony") 56 | add_deps(table.unpack(benchmarks_targets)) 57 | on_run(function (target) 58 | import("core.base.option") 59 | local script_path = "./benchmarks/pingpong.py" 60 | local args = option.get("arguments") or {} 61 | table.insert(args, 1, "--xmake") 62 | table.insert(args, 2, "y") 63 | os.execv("python3", {script_path, unpack(args)}) 64 | end) 65 | 66 | target("clean") 67 | on_clean(function () 68 | os.rm("build") 69 | end) 70 | set_kind("phony") 71 | -------------------------------------------------------------------------------- /benchmarks/pong_asio.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | using asio::ip::tcp; 11 | using asio::use_awaitable; 12 | 13 | asio::awaitable pong(asio::ip::tcp::socket client, int block_size) try { 14 | std::string content(block_size, 'x'); 15 | for(;;) { 16 | auto n = co_await asio::async_read(client, asio::buffer(content), use_awaitable); 17 | co_await asio::async_write(client, asio::buffer(content, n), use_awaitable); 18 | } 19 | } catch(std::exception &e) { 20 | // Ignore EOF. 21 | } 22 | 23 | asio::awaitable server(auto &acceptor, int block_size, int session_count) try { 24 | auto executor = acceptor.get_executor(); 25 | for(int i = 0; i < session_count; ++i) { 26 | auto client = co_await acceptor.async_accept(use_awaitable); 27 | asio::co_spawn( 28 | executor, 29 | pong(std::move(client), block_size), 30 | asio::detached); 31 | } 32 | } catch(std::exception &e) { 33 | } 34 | 35 | // Enable io_uring: -DASIO_HAS_IO_URING -DASIO_DISABLE_EPOLL 36 | int main(int argc, char *argv[]) { 37 | if(argc <= 4) { 38 | auto message = std::format( 39 | "usage: {} ", argv[0]); 40 | std::cerr << message << std::endl; 41 | return -1; 42 | } 43 | auto atoies = [&](auto ...idxes) { return std::tuple{atoi(argv[idxes])...}; }; 44 | auto [port, thread_count, block_size, session_count] = atoies(1, 2, 3, 4); 45 | // auto sb = asio::detail::signal_blocker(); 46 | asio::io_context ioc; 47 | asio::signal_set s(ioc, SIGINT); 48 | s.async_wait([](auto, auto) { std::quick_exit(0); }); 49 | tcp::acceptor acceptor(ioc, {tcp::v4(), static_cast(port)}); 50 | asio::co_spawn( 51 | ioc, 52 | server(acceptor, block_size, session_count), 53 | asio::detached); 54 | 55 | { 56 | std::vector threads(thread_count); 57 | for(auto &&j : threads) j = std::jthread([&] { ioc.run(); }); 58 | } 59 | 60 | std::cout << "done." << std::endl; 61 | } 62 | -------------------------------------------------------------------------------- /benchmarks/pingpong.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import time 3 | import argparse 4 | import signal 5 | import os 6 | import sys 7 | 8 | parser = argparse.ArgumentParser(description="Run ping-pong tests with different servers.") 9 | parser.add_argument("--server", choices=["uring_exec", "asio"], default="uring_exec") 10 | parser.add_argument("--client", choices=["default", "when_any"], default="default") 11 | parser.add_argument("--xmake", choices=["y", "n"], default="n") 12 | args = parser.parse_args() 13 | 14 | server_name = "pong" if args.server == "uring_exec" else "pong_asio" 15 | client_name = "ping" if args.client == "default" else "ping_when_any" 16 | 17 | use_xmake = (args.xmake == "y") 18 | 19 | blocksize = "16384" 20 | timeout = "10" # s 21 | port = "8848" 22 | 23 | def signal_handler(sig, frame): 24 | print("Ctrl+C detected. Sending SIGINT to child processes...") 25 | for handle in [server_handle, client_handle]: 26 | if handle: 27 | try: os.killpg(handle.pid, signal.SIGINT) 28 | except: pass 29 | sys.exit(0) 30 | 31 | signal.signal(signal.SIGINT, signal_handler) 32 | 33 | print("Server:", args.server) 34 | print("Client:", args.client) 35 | print("==========") 36 | for thread in [1, 2, 4, 8]: 37 | for session in [10, 100, 1000, 10000, 100000]: 38 | print(">> thread:", thread, "session:", session) 39 | time.sleep(1) 40 | common_args = [port, str(thread), blocksize, str(session)] 41 | if use_xmake: 42 | server_cmd = ["xmake", "run", server_name] 43 | client_cmd = ["xmake", "run", client_name] 44 | else: 45 | server_cmd = ["./build/" + server_name] 46 | client_cmd = ["./build/" + client_name] 47 | server_cmd += common_args 48 | client_cmd += common_args + [timeout] 49 | # Unfortunately, `xmake run` will fork two different processes for server. 50 | # We need a group to kill it/them. 51 | server_handle = subprocess.Popen(server_cmd, process_group=0) 52 | time.sleep(.256) # Start first. 53 | client_handle = subprocess.Popen(client_cmd, process_group=0) 54 | time.sleep(int(timeout)) 55 | os.killpg(server_handle.pid, signal.SIGINT) 56 | server_handle.wait() 57 | client_handle.wait() 58 | print("==========") 59 | -------------------------------------------------------------------------------- /examples/hello_world.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "uring_exec.hpp" 6 | 7 | using uring_exec::io_uring_exec; 8 | 9 | int main() { 10 | constexpr auto test_file_name = "/tmp/jojo"; 11 | int fd = (::unlink(test_file_name), ::open(test_file_name, O_RDWR|O_TRUNC|O_CREAT, 0666)); 12 | if(fd < 0) { 13 | std::cerr << ::strerror(errno) , std::abort(); 14 | } 15 | 16 | io_uring_exec uring(512); 17 | stdexec::scheduler auto scheduler = uring.get_scheduler(); 18 | std::jthread j {[&](std::stop_token stop_token) { uring.run(stop_token); }}; 19 | 20 | auto s1 = 21 | stdexec::schedule(scheduler) 22 | | stdexec::then([] { 23 | std::cout << "hello!" << std::endl; 24 | return 19260816; 25 | }) 26 | | stdexec::then([](int v) { 27 | return v+1; 28 | }); 29 | 30 | auto s2 = 31 | stdexec::schedule(scheduler) 32 | | stdexec::let_value([] { 33 | std::cout << "world!" << std::endl; 34 | return stdexec::just(std::array {"dio"}); // '\n' 35 | }) 36 | | stdexec::let_value([scheduler, fd](auto &&buf) { 37 | return 38 | uring_exec::async_write(scheduler, fd, buf.data(), buf.size() - 1) 39 | | stdexec::let_value([&](auto written_bytes) { 40 | return uring_exec::async_read(scheduler, fd, buf.data(), written_bytes); 41 | }) 42 | | stdexec::then([&buf](auto read_bytes) { 43 | auto iter = std::ostream_iterator{std::cout}; 44 | [&](auto &&...views) { (std::ranges::copy(views, iter), ...); } 45 | ("read: [", buf | std::views::take(read_bytes), "]\n"); 46 | return read_bytes; 47 | }); 48 | }); 49 | 50 | // exec::async_scope scope; 51 | // scope.spawn(std::move(s1) | stdexec::then([](...) {})); 52 | // scope.spawn(std::move(s2) | stdexec::then([](...) {})); 53 | // stdexec::sync_wait(scope.on_empty()); 54 | 55 | auto a = 56 | stdexec::when_all(std::move(s1), std::move(s2)); 57 | auto [v1, v2] = stdexec::sync_wait(std::move(a)).value(); 58 | std::cout << "s1: " << v1 << std::endl 59 | << "s2: " << v2 << std::endl; 60 | } 61 | -------------------------------------------------------------------------------- /examples/signal_handling.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "uring_exec.hpp" 6 | #include "exec/when_any.hpp" 7 | 8 | using uring_exec::io_uring_exec; 9 | using namespace std::chrono_literals; 10 | using namespace std::chrono; 11 | 12 | // Modified from `examples/timer.cpp`. 13 | int main() { 14 | io_uring_exec uring(512); 15 | stdexec::scheduler auto scheduler = uring.get_scheduler(); 16 | 17 | std::cout << "start." << std::endl; 18 | 19 | auto s1 = 20 | stdexec::schedule(scheduler) 21 | | stdexec::let_value([=](auto &&...) { 22 | return uring_exec::async_wait(scheduler, 2s); 23 | }) 24 | | stdexec::let_value([=](auto &&...) { 25 | std::cout << "s1:2s" << std::endl; 26 | return stdexec::just(); 27 | }) 28 | | stdexec::let_value([=](auto &&...) { 29 | return uring_exec::async_wait(scheduler, 2s); 30 | }) 31 | | stdexec::let_value([=](auto &&...) { 32 | std::cout << "s1:4s" << std::endl; 33 | return stdexec::just(); 34 | }); 35 | 36 | auto s2 = 37 | stdexec::schedule(scheduler) 38 | | stdexec::let_value([=](auto &&...) { 39 | return uring_exec::async_wait(scheduler, 1s); 40 | }) 41 | | stdexec::let_value([=](auto &&...){ 42 | std::cout << "s2:1s" << std::endl; 43 | return stdexec::just(); 44 | }) 45 | | stdexec::let_value([=](auto &&...) { 46 | return uring_exec::async_wait(scheduler, 2s); 47 | }) 48 | | stdexec::let_value([=](auto &&...) { 49 | std::cout << "s2:3s" << std::endl; 50 | return stdexec::just(); 51 | }); 52 | 53 | stdexec::sender auto signal_watchdog = 54 | uring_exec::async_sigwait(scheduler, std::array {SIGINT, SIGUSR1}); 55 | 56 | auto sb = uring_exec::signal_blocker(); 57 | 58 | std::jthread j([&](auto token) { uring.run(token); }); 59 | // $ kill -USR1 $(pgrep signal_handling) 60 | stdexec::sync_wait( 61 | exec::when_any( 62 | stdexec::when_all(std::move(s1), std::move(s2)) 63 | | stdexec::then([](auto &&...) { std::cout << "timer!" << std::endl; }), 64 | stdexec::starts_on(scheduler, std::move(signal_watchdog)) 65 | | stdexec::then([](auto &&...) { std::cout << "signal!" << std::endl; }) 66 | ) 67 | ); 68 | std::cout << "bye." << std::endl; 69 | } 70 | -------------------------------------------------------------------------------- /examples/echo_sender.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "uring_exec.hpp" 7 | 8 | using uring_exec::io_uring_exec; 9 | 10 | // Not important things. 11 | using uring_exec::utils::make_server; 12 | using uring_exec::utils::defer; 13 | 14 | stdexec::sender 15 | auto echo(io_uring_exec::scheduler scheduler, int client_fd) { 16 | return 17 | stdexec::just(std::array{}) 18 | | stdexec::let_value([=](auto &buf) { 19 | return 20 | uring_exec::async_read(scheduler, client_fd, buf.data(), buf.size()) 21 | | stdexec::then([=, &buf](int read_bytes) { 22 | auto copy = std::ranges::copy; 23 | auto view = buf | std::views::take(read_bytes); 24 | auto to_console = std::ostream_iterator{std::cout}; 25 | copy(view, to_console); 26 | return read_bytes; 27 | }) 28 | | stdexec::let_value([=, &buf](int read_bytes) { 29 | return uring_exec::async_write(scheduler, client_fd, buf.data(), read_bytes); 30 | }) 31 | | stdexec::let_value([=, &buf](int written_bytes) { 32 | return stdexec::just(written_bytes == 0 || buf[0] == '@'); 33 | }); 34 | }) 35 | | exec::repeat_effect_until() 36 | | stdexec::let_value([=] { 37 | std::cout << "Closing client..." << std::endl; 38 | return uring_exec::async_close(scheduler, client_fd) | stdexec::then([](...){}); 39 | }); 40 | } 41 | 42 | stdexec::sender 43 | auto server(io_uring_exec::scheduler scheduler, int server_fd, exec::async_scope &scope) { 44 | return 45 | uring_exec::async_accept(scheduler, server_fd, nullptr, nullptr, 0) 46 | | stdexec::let_value([=, &scope](int client_fd) { 47 | scope.spawn(echo(scheduler, client_fd)); 48 | return stdexec::just(false); 49 | }) 50 | | exec::repeat_effect_until(); 51 | } 52 | 53 | int main() { 54 | auto server_fd = make_server({.port=8848}); 55 | auto server_fd_cleanup = defer([=] { close(server_fd); }); 56 | 57 | io_uring_exec uring({.uring_entries=512}); 58 | exec::async_scope scope; 59 | 60 | stdexec::scheduler auto scheduler = uring.get_scheduler(); 61 | 62 | scope.spawn( 63 | stdexec::schedule(scheduler) 64 | | stdexec::let_value([=, &scope] { 65 | return server(scheduler, server_fd, scope); 66 | }) 67 | ); 68 | 69 | uring.run(); 70 | } 71 | -------------------------------------------------------------------------------- /benchmarks/README: -------------------------------------------------------------------------------- 1 | !!!WORK IN PROGRESS!!! 2 | !!!WORK IN PROGRESS!!! 3 | !!!WORK IN PROGRESS!!! 4 | 5 | GCC works. But Clang has some problems. ¯\_(ツ)_/¯ 6 | Clang works! 7 | 8 | ========== ping-pong ========== 9 | 10 | `ping` is a client. 11 | `pong` is a server. 12 | 13 | Usage: 14 | pong 15 | ping 16 | 17 | Example: 18 | ./build/pong 8848 5 16384 50 19 | ./build/ping 8848 5 16384 50 10 20 | 21 | One-click script: 22 | * xmake: `xmake build benchmarks && xmake run benchmarks`. 23 | * make: `make benchmark_script`. 24 | It is recommended to use **xmake** to run benchmarks. 25 | All commands should be run from the root directory. 26 | 27 | Note: 28 | * `pong_asio` is also a server, but it uses Asio. You can use it to compare performance. 29 | For a fair comparison, make sure to enable the `-DASIO_HAS_IO_URING` and `-DASIO_DISABLE_EPOLL` macros. 30 | (`epoll` is toooo fast! For the current kernel implementation, `io_uring` can't beat it.) 31 | Use `xmake run benchmarks --server asio` to benchmark. 32 | * `ping_when_any` is also a client, but it uses `exec::when_any`. You can use it to compare polling performance. 33 | Although `uring_exec` supports per-I/O-operation cancellation, it may reduce performance. 34 | Use `xmake run benchmarks --client when_any` to benchmark. 35 | 36 | Here is my benchmark report on: 37 | * {Linux v6.4.8} 38 | * {AMD 5800H, 16 GB} 39 | * {uring_exec 6d77952, asio 62481a2} 40 | * {gcc v13.2.0 -O3} 41 | 42 | blocksize = 16384 43 | timeout = 10s 44 | throughput unit = GiB/s 45 | 46 | | threads / sessions | asio (io_uring) | uring_exec | asio (epoll) | 47 | | ------------------ | --------------- | ---------- | ------------ | 48 | | 1 / 10 | 3.517 | 3.717 | 3.098 | 49 | | 1 / 100 | 3.600 | 4.026 | 3.052 | 50 | | 1 / 1000 | 1.462 | 1.620 | 1.320 | 51 | | 1 / 10000 | 1.432 | 1.829 | 1.321 | 52 | | 1 / 100000 | 0.349 | 0.397 | 0.327 | 53 | | 2 / 10 | 1.639 | 3.376 | 4.610 | 54 | | 2 / 100 | 2.724 | 3.489 | 5.013 | 55 | | 2 / 1000 | 1.329 | 1.889 | 1.772 | 56 | | 2 / 10000 | 1.334 | 1.770 | 1.833 | 57 | | 2 / 100000 | 0.559 | 1.675 | 0.545 | 58 | | 4 / 10 | 1.708 | 3.106 | 6.279 | 59 | | 4 / 100 | 2.538 | 3.226 | 8.744 | 60 | | 4 / 1000 | 1.312 | 4.796 | 2.144 | 61 | | 4 / 10000 | 1.317 | 1.978 | 2.150 | 62 | | 4 / 100000 | 1.171 | 3.598 | 2.011 | 63 | | 8 / 10 | 0.936 | 2.430 | 2.370 | 64 | | 8 / 100 | 2.031 | 2.518 | 7.979 | 65 | | 8 / 1000 | 1.120 | 4.962 | 2.005 | 66 | | 8 / 10000 | 1.093 | 6.662 | 2.118 | 67 | | 8 / 100000 | 1.075 | 5.971 | 1.900 | 68 | -------------------------------------------------------------------------------- /benchmarks/pong.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "uring_exec.hpp" 10 | 11 | using uring_exec::io_uring_exec; 12 | constexpr auto noop = [](auto &&...) {}; 13 | 14 | stdexec::sender 15 | auto pong(io_uring_exec::scheduler scheduler, int client_fd, int blocksize) { 16 | return 17 | stdexec::just(std::vector(blocksize)) 18 | | stdexec::let_value([=](auto &buf) { 19 | return 20 | uring_exec::async_read(scheduler, client_fd, buf.data(), buf.size()) 21 | | stdexec::let_value([=, &buf](int read_bytes) { 22 | return uring_exec::async_write(scheduler, client_fd, buf.data(), read_bytes); 23 | }) 24 | | stdexec::let_value([=](int written_bytes) { 25 | return stdexec::just(written_bytes == 0); 26 | }) 27 | | exec::repeat_effect_until(); 28 | }) 29 | | stdexec::upon_error(noop) 30 | | stdexec::let_value([=] { 31 | return 32 | uring_exec::async_close(scheduler, client_fd); 33 | }) 34 | | stdexec::then(noop); 35 | } 36 | 37 | stdexec::sender 38 | auto server(io_uring_exec::scheduler scheduler, exec::async_scope &scope, 39 | int server_fd, int blocksize, int sessions) { 40 | return 41 | stdexec::just() 42 | | stdexec::let_value([=, &scope] { 43 | return 44 | uring_exec::async_accept(scheduler, server_fd, nullptr, nullptr, 0) 45 | | stdexec::then([=, &scope](int client_fd) mutable { 46 | scope.spawn(pong(scheduler, client_fd, blocksize)); 47 | }) 48 | | exec::repeat_n(sessions); 49 | }) 50 | | stdexec::upon_error(noop); 51 | } 52 | 53 | int main(int argc, char *argv[]) { 54 | if(argc <= 4) { 55 | auto message = std::format( 56 | "usage: {} ", argv[0]); 57 | std::cerr << message << std::endl; 58 | return -1; 59 | } 60 | auto atoies = [&](auto ...idxes) { return std::tuple{atoi(argv[idxes])...}; }; 61 | auto [port, threads, blocksize, sessions] = atoies(1, 2, 3, 4); 62 | 63 | auto sb = uring_exec::signal_blocker(SIGINT); 64 | auto server_fd = uring_exec::make_server({.port=port}); 65 | io_uring_exec uring({.uring_entries=512}); 66 | exec::async_scope scope; 67 | 68 | std::vector thread_pool(threads); 69 | for(auto &&j : thread_pool) { 70 | j = std::jthread([&](auto stop_token) { uring.run(stop_token); }); 71 | } 72 | 73 | stdexec::scheduler auto scheduler = uring.get_scheduler(); 74 | stdexec::sender auto s = stdexec::starts_on(scheduler, 75 | server(scheduler, scope, server_fd, blocksize, sessions)); 76 | stdexec::sender auto f = stdexec::starts_on(scheduler, 77 | uring_exec::async_close(scheduler, server_fd)); 78 | auto sequence = [](stdexec::sender auto ...senders) { 79 | (stdexec::sync_wait(std::move(senders)), ...); 80 | }; 81 | sequence(std::move(s), scope.on_empty(), std::move(f)); 82 | std::cout << "done." << std::endl; 83 | } 84 | -------------------------------------------------------------------------------- /include/uring_exec/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | namespace uring_exec { 16 | inline namespace utils { 17 | 18 | // C-style check for syscall. 19 | // inline void check(int cond, const char *reason) { 20 | // if(!cond) [[likely]] return; 21 | // perror(reason); 22 | // abort(); 23 | // } 24 | 25 | // C++-style check for syscall. 26 | // Failed on ret < 0 by default. 27 | // 28 | // INT_ASYNC_CHECK: helper for liburing (-ERRNO) and other syscalls (-1). 29 | // It may break generic programming (forced to int). 30 | template , auto V = 0, bool INT_ASYNC_CHECK = true> 31 | struct nofail { 32 | std::string_view reason; 33 | 34 | // Examples: 35 | // fstat(...) | nofail("fstat"); // Forget the if-statement and ret! 36 | // int fd = open(...) | nofail("open"); // If actually need a ret, here you are! 37 | friend decltype(auto) operator|(auto &&ret, nofail nf) { 38 | if(Comp{}(ret, V)) [[unlikely]] { 39 | // Hack errno. 40 | if constexpr (INT_ASYNC_CHECK) { 41 | using T = std::decay_t; 42 | static_assert(std::is_convertible_v); 43 | // -ERRNO 44 | if(ret != -1) errno = -ret; 45 | } 46 | perror(nf.reason.data()); 47 | std::terminate(); 48 | } 49 | return std::forward(ret); 50 | }; 51 | }; 52 | 53 | // Make clang happy. 54 | nofail(...) -> nofail, 0, true>; 55 | 56 | // Go-style, move-safe defer. 57 | [[nodiscard("defer() is not allowed to be temporary.")]] 58 | inline auto defer(auto func) { 59 | auto _0x1 = std::uintptr_t {0x1}; 60 | // reinterpret_cast is not a constexpr. 61 | auto make_STL_happy = reinterpret_cast(_0x1); 62 | auto make_dtor_happy = [f = std::move(func)](...) { f(); }; 63 | using Defer = std::unique_ptr; 64 | return Defer{make_STL_happy, std::move(make_dtor_happy)}; 65 | } 66 | 67 | // For make_server(). 68 | struct make_server_option_t { 69 | int port {8848}; 70 | int backlog {128}; 71 | bool nonblock {false}; 72 | bool reuseaddr {true}; 73 | bool reuseport {false}; 74 | }; 75 | 76 | // Do some boring stuff and return a server fd. 77 | inline int make_server(make_server_option_t option) { 78 | int socket_flag = option.nonblock ? SOCK_NONBLOCK : 0; 79 | int socket_fd = socket(AF_INET, SOCK_STREAM, socket_flag) | nofail("socket"); 80 | 81 | auto setsock = [enable = 1, fd = socket_fd](int optname) { 82 | setsockopt(fd, SOL_SOCKET, optname, &enable, sizeof(int)) | nofail("setsockopt"); 83 | }; 84 | if(option.reuseaddr) setsock(SO_REUSEADDR); 85 | if(option.reuseport) setsock(SO_REUSEPORT); 86 | 87 | sockaddr_in addr {}; 88 | addr.sin_family = AF_INET; 89 | addr.sin_port = htons(option.port); 90 | addr.sin_addr.s_addr = htonl(INADDR_ANY); 91 | 92 | auto no_alias_addr = reinterpret_cast(&addr); 93 | 94 | bind(socket_fd, no_alias_addr, sizeof(addr)) | nofail("bind"); 95 | 96 | listen(socket_fd, option.backlog) | nofail("listen"); 97 | 98 | return socket_fd; 99 | } 100 | 101 | // For signal_blocker(). Inclusive is default mode. 102 | // Example: 103 | // // Block all signals except SIGINT and SIGUSR1. 104 | // auto sb = signal_blocker(std::array {SIGINT, SIGUSR1}); 105 | struct sigmask_exclusive_t: std::true_type {}; 106 | struct sigmask_inclusive_t: std::false_type {}; 107 | inline constexpr auto sigmask_exclusive = sigmask_exclusive_t{}; 108 | inline constexpr auto sigmask_inclusive = sigmask_inclusive_t{}; 109 | 110 | // Block all/some signals in ctor. 111 | // Restore previous signals in dtor (or .reset()). 112 | // Examples: 113 | // // Block all signals. 114 | // auto sb = signal_blocker(); 115 | // 116 | // // Block a single SIGINT signal. 117 | // auto sb = signal_blocker(SIGINT); 118 | // 119 | // // Block SIGINT and SIGUSR1 singls. (std::vector or other ranges are also acceptable.) 120 | // auto sb = signal_blocker(std::array {SIGINT, SIGUSR1}); 121 | // 122 | // // Block all signals except SIGINT and SIGUSR1. 123 | // auto sb = signal_blocker(std::array {SIGINT, SIGUSR1}); 124 | template > // Can be a range, or a single signal value type. 126 | inline auto signal_blocker(Container &&signals = {}) { 127 | /// ctor 128 | sigset_t new_mask, old_mask; 129 | auto empty_signals_f = [&] { 130 | if constexpr (std::ranges::range) return std::size(signals) == 0; 131 | else return false; 132 | }; 133 | bool init_fill = empty_signals_f() || exclusive; 134 | (init_fill ? sigfillset : sigemptyset)(&new_mask); 135 | auto modify = (exclusive ? sigdelset : sigaddset); 136 | if constexpr (std::ranges::range) { 137 | for(auto signal : signals) modify(&new_mask, signal); 138 | } else { 139 | modify(&new_mask, signals); 140 | } 141 | sigemptyset(&old_mask); 142 | // The use of sigprocmask() is unspecified in a multithreaded process; see pthread_sigmask(3). 143 | pthread_sigmask(SIG_BLOCK, &new_mask, &old_mask); 144 | 145 | /// dtor 146 | return defer([old_mask] { 147 | pthread_sigmask(SIG_SETMASK, &old_mask, 0); 148 | }); 149 | } 150 | 151 | } // namespace utils 152 | } // namespace uring_exec 153 | -------------------------------------------------------------------------------- /benchmarks/ping.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "uring_exec.hpp" 12 | 13 | using uring_exec::io_uring_exec; 14 | constexpr auto noop = [](auto &&...) {}; 15 | 16 | stdexec::sender 17 | auto ping(io_uring_exec::scheduler scheduler, 18 | int client_fd, int blocksize, auto stop_token) { 19 | return 20 | stdexec::just(std::vector(blocksize, 'x'), size_t{}, size_t{}) 21 | | stdexec::let_value([=](auto &buf, auto &r, auto &w) { 22 | return 23 | stdexec::just() 24 | | stdexec::let_value([=, &buf] { 25 | return uring_exec::async_write(scheduler, client_fd, buf.data(), buf.size()); 26 | }) 27 | | stdexec::let_value([=, &buf, &w](int written_bytes) { 28 | w += written_bytes; 29 | return uring_exec::async_read(scheduler, client_fd, buf.data(), written_bytes); 30 | }) 31 | | stdexec::let_value([=, &r](int read_bytes) { 32 | r += read_bytes; 33 | return stdexec::just(stop_token.stop_requested()); 34 | }) 35 | | exec::repeat_effect_until() 36 | | stdexec::upon_error(noop) 37 | | stdexec::let_value([&] { 38 | return stdexec::just(r, w); 39 | }); 40 | }); 41 | } 42 | 43 | stdexec::sender 44 | auto client(io_uring_exec::scheduler scheduler, 45 | auto endpoint, int blocksize, 46 | std::atomic &r, std::atomic &w, 47 | auto stop_token) { 48 | auto make_addr = [](auto endpoint) { 49 | auto [host, port] = endpoint; 50 | sockaddr_in addr_in {}; 51 | addr_in.sin_family = AF_INET; 52 | inet_pton(AF_INET, host, &addr_in.sin_addr); 53 | addr_in.sin_port = htons(port); 54 | auto addr = std::bit_cast(addr_in); 55 | return addr; 56 | }; 57 | return 58 | stdexec::just() 59 | | stdexec::let_value([=] { 60 | return uring_exec::async_socket(scheduler, AF_INET, SOCK_STREAM, IPPROTO_TCP, 0); 61 | }) 62 | | stdexec::let_value([=, addr = make_addr(endpoint)](int client_fd) { 63 | return uring_exec::async_connect(scheduler, client_fd, &addr, sizeof addr) 64 | | stdexec::then([=](auto&&) { return client_fd; }); 65 | }) 66 | | stdexec::let_value([=, &r, &w](int client_fd) { 67 | return ping(scheduler, client_fd, blocksize, stop_token) 68 | | stdexec::then([=, &r, &w](size_t read_bytes, size_t written_bytes) { 69 | r.fetch_add(read_bytes); 70 | w.fetch_add(written_bytes); 71 | return client_fd; 72 | }); 73 | }) 74 | | stdexec::let_value([=](int client_fd) { 75 | return uring_exec::async_close(scheduler, client_fd); 76 | }) 77 | | stdexec::upon_error(noop) 78 | | stdexec::then(noop); 79 | } 80 | 81 | int main(int argc, char *argv[]) { 82 | if(argc <= 5) { 83 | auto message = std::format( 84 | "usage: {} ", argv[0]); 85 | std::cerr << message << std::endl; 86 | return -1; 87 | } 88 | auto host = "127.0.0.1"; 89 | auto atoies = [&](auto ...idxes) { return std::tuple{atoi(argv[idxes])...}; }; 90 | auto [port, threads, blocksize, sessions, timeout] = atoies(1, 2, 3, 4, 5); 91 | assert(timeout >= 1); 92 | 93 | auto sb = uring_exec::signal_blocker(SIGINT); 94 | io_uring_exec uring({.uring_entries=512}); 95 | 96 | std::vector thread_pool(threads); 97 | for(auto &&j : thread_pool) { 98 | j = std::jthread([&](auto stop_token) { uring.run(stop_token); }); 99 | } 100 | 101 | stdexec::inplace_stop_source iss; 102 | exec::async_scope scope; 103 | stdexec::scheduler auto scheduler = uring.get_scheduler(); 104 | auto endpoint = std::tuple(host, port); 105 | std::atomic r {}; 106 | std::atomic w {}; 107 | 108 | stdexec::sender auto deadline = 109 | stdexec::schedule(scheduler) 110 | | stdexec::let_value([=] { 111 | return uring_exec::async_wait(scheduler, std::chrono::seconds(timeout)); 112 | }) 113 | | stdexec::then([&](auto&&) { iss.request_stop(); }); 114 | scope.spawn(std::move(deadline)); 115 | 116 | for(auto n = sessions; n--;) { 117 | stdexec::sender auto s = 118 | stdexec::schedule(scheduler) 119 | | stdexec::let_value([=, &r, &w, &iss] { 120 | return client(scheduler, endpoint, blocksize, r, w, iss.get_token()); 121 | }) 122 | | stdexec::then(noop); 123 | scope.spawn(s); 124 | } 125 | 126 | stdexec::sync_wait(scope.on_empty()); 127 | 128 | double read_bytes = r.load(); 129 | double written_bytes = w.load(); 130 | 131 | auto stringify = [](double bytes, int timeout) { 132 | bytes /= timeout; 133 | auto conv = [&, base = 1024] { 134 | for(auto i = 0; i < 6; i++) { 135 | if(bytes < base) return std::tuple(bytes, "BKMGTP"[i]); 136 | bytes /= base; 137 | } 138 | return std::tuple(bytes, 'E'); 139 | }; 140 | auto [good_bytes, unit] = conv(); 141 | auto suffix = ('B'==unit ? "" : "iB"); 142 | return std::format("{:.3f} {}{}", good_bytes, unit, suffix); 143 | }; 144 | auto println = [](auto ...args) { 145 | (std::cout << ... << args) << std::endl; 146 | }; 147 | 148 | println("done."); 149 | println("read: ", stringify(read_bytes, 1)); 150 | println("write: ", stringify(written_bytes, 1)); 151 | println("throughput (read): ", stringify(read_bytes, timeout), "/s"); 152 | println("throughput (write): ", stringify(written_bytes, timeout), "/s"); 153 | } 154 | -------------------------------------------------------------------------------- /benchmarks/ping_when_any.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "uring_exec.hpp" 12 | 13 | using uring_exec::io_uring_exec; 14 | constexpr auto noop = [](auto &&...) {}; 15 | 16 | stdexec::sender 17 | auto ping(io_uring_exec::scheduler scheduler, 18 | int client_fd, int blocksize) { 19 | return 20 | stdexec::just(std::vector(blocksize, 'x'), size_t{}, size_t{}) 21 | | stdexec::let_value([=](auto &buf, auto &r, auto &w) { 22 | return 23 | stdexec::just() 24 | | stdexec::let_value([=, &buf] { 25 | return uring_exec::async_write(scheduler, client_fd, buf.data(), buf.size()); 26 | }) 27 | | stdexec::let_value([=, &buf, &w](int written_bytes) { 28 | w += written_bytes; 29 | return uring_exec::async_read(scheduler, client_fd, buf.data(), written_bytes); 30 | }) 31 | | stdexec::let_value([&r, &w](int read_bytes) { 32 | r += read_bytes; 33 | return stdexec::just(r, w); 34 | }) 35 | | stdexec::let_error([&](auto &&) { 36 | return stdexec::just(r, w); 37 | }) 38 | | stdexec::let_stopped([&] { 39 | return stdexec::just(r, w); 40 | }); 41 | }); 42 | } 43 | 44 | stdexec::sender 45 | auto client(io_uring_exec::scheduler scheduler, 46 | auto endpoint, int blocksize, 47 | std::atomic &r, std::atomic &w) { 48 | auto make_addr = [](auto endpoint) { 49 | auto [host, port] = endpoint; 50 | sockaddr_in addr_in {}; 51 | addr_in.sin_family = AF_INET; 52 | inet_pton(AF_INET, host, &addr_in.sin_addr); 53 | addr_in.sin_port = htons(port); 54 | auto addr = std::bit_cast(addr_in); 55 | return addr; 56 | }; 57 | return 58 | stdexec::just() 59 | | stdexec::let_value([=] { 60 | return uring_exec::async_socket(scheduler, AF_INET, SOCK_STREAM, IPPROTO_TCP, 0); 61 | }) 62 | | stdexec::let_value([=, addr = make_addr(endpoint)](int client_fd) { 63 | return uring_exec::async_connect(scheduler, client_fd, &addr, sizeof addr) 64 | | stdexec::let_value([=](auto&&) { 65 | return stdexec::just(client_fd, size_t{}, size_t{}); 66 | }); 67 | }) 68 | | stdexec::let_value([=, &r, &w](int client_fd, size_t &client_read, size_t &client_written) { 69 | auto collect = [&, client_fd](auto &&...) { 70 | constexpr auto mo = std::memory_order::relaxed; 71 | r.fetch_add(client_read, mo); 72 | w.fetch_add(client_written, mo); 73 | return stdexec::just(client_fd); 74 | }; 75 | return 76 | ping(scheduler, client_fd, blocksize) 77 | | stdexec::let_value([&](size_t read_bytes, size_t written_bytes) { 78 | client_read += read_bytes; 79 | client_written += written_bytes; 80 | return stdexec::just(false); 81 | }) 82 | | exec::repeat_effect_until() 83 | | stdexec::let_value(collect) 84 | | stdexec::let_stopped(collect) 85 | | stdexec::let_error(collect); 86 | }) 87 | | stdexec::let_value([=](int client_fd) { 88 | return uring_exec::async_close(scheduler, client_fd); 89 | }) 90 | | stdexec::upon_error(noop) 91 | | stdexec::upon_stopped(noop) 92 | | stdexec::then(noop); 93 | } 94 | 95 | int main(int argc, char *argv[]) { 96 | if(argc <= 5) { 97 | auto message = std::format( 98 | "usage: {} ", argv[0]); 99 | std::cerr << message << std::endl; 100 | return -1; 101 | } 102 | auto host = "127.0.0.1"; 103 | auto atoies = [&](auto ...idxes) { return std::tuple{atoi(argv[idxes])...}; }; 104 | auto [port, threads, blocksize, sessions, timeout] = atoies(1, 2, 3, 4, 5); 105 | assert(timeout >= 1); 106 | 107 | auto sb = uring_exec::signal_blocker(SIGINT); 108 | io_uring_exec uring({.uring_entries=512}); 109 | 110 | std::vector thread_pool(threads); 111 | for(auto &&j : thread_pool) { 112 | j = std::jthread([&](auto stop_token) { uring.run(stop_token); }); 113 | } 114 | 115 | exec::async_scope scope; 116 | stdexec::scheduler auto scheduler = uring.get_scheduler(); 117 | auto endpoint = std::tuple(host, port); 118 | std::atomic r {}; 119 | std::atomic w {}; 120 | 121 | for(auto n = sessions; n--;) { 122 | stdexec::sender auto s = 123 | stdexec::starts_on(scheduler, client(scheduler, endpoint, blocksize, r, w)); 124 | scope.spawn(std::move(s)); 125 | } 126 | 127 | stdexec::sender auto deadline = 128 | stdexec::schedule(scheduler) 129 | | stdexec::let_value([=] { 130 | return uring_exec::async_wait(scheduler, std::chrono::seconds(timeout)); 131 | }) 132 | | stdexec::then([&](auto&&) { scope.request_stop(); }); 133 | 134 | stdexec::sender auto timed_execution = exec::when_any(std::move(deadline), scope.on_empty()); 135 | stdexec::sync_wait(std::move(timed_execution)); 136 | 137 | double read_bytes = r.load(); 138 | double written_bytes = w.load(); 139 | 140 | auto stringify = [](double bytes, int timeout) { 141 | bytes /= timeout; 142 | auto conv = [&, base = 1024] { 143 | for(auto i = 0; i < 6; i++) { 144 | if(bytes < base) return std::tuple(bytes, "BKMGTP"[i]); 145 | bytes /= base; 146 | } 147 | return std::tuple(bytes, 'E'); 148 | }; 149 | auto [good_bytes, unit] = conv(); 150 | auto suffix = ('B'==unit ? "" : "iB"); 151 | return std::format("{:.3f} {}{}", good_bytes, unit, suffix); 152 | }; 153 | auto println = [](auto ...args) { 154 | (std::cout << ... << args) << std::endl; 155 | }; 156 | 157 | println("done."); 158 | println("read: ", stringify(read_bytes, 1)); 159 | println("write: ", stringify(written_bytes, 1)); 160 | println("throughput (read): ", stringify(read_bytes, timeout), "/s"); 161 | println("throughput (write): ", stringify(written_bytes, timeout), "/s"); 162 | } 163 | -------------------------------------------------------------------------------- /include/uring_exec/detail.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | namespace uring_exec { 8 | namespace detail { 9 | 10 | ////////////////////////////////////////////////////////////////////// Immovable guarantee 11 | 12 | // Operations in stdexec are required to be address-stable. 13 | struct immovable { 14 | immovable() = default; 15 | immovable(immovable &&) = delete; 16 | }; 17 | 18 | ////////////////////////////////////////////////////////////////////// Atomic intrusive queue 19 | 20 | template 21 | concept intrusive = 22 | std::derived_from 23 | && std::derived_from; 24 | 25 | template 26 | struct intrusive_queue; 27 | 28 | template T, 30 | Node* Node::*Next> 31 | requires requires(T t) { t.*Next; } 32 | struct intrusive_queue { 33 | using node_type = Node; 34 | using element_type = T; 35 | 36 | void push(T *op) noexcept { 37 | auto node = _head.load(read_mo); 38 | do { 39 | op->*Next = node; 40 | } while(!_head.compare_exchange_weak(node, op, write_mo, read_mo)); 41 | } 42 | 43 | [[nodiscard]] 44 | T* move_all() noexcept { 45 | auto node = _head.load(read_mo); 46 | while(!_head.compare_exchange_weak(node, nullptr, write_mo, read_mo)); 47 | return static_cast(node); 48 | } 49 | 50 | // Push until predicate() == false to queue. 51 | void push_all(T *first, auto predicate) noexcept { 52 | Node *last = first; 53 | if(!predicate(first)) return; 54 | while(predicate(static_cast(last->*Next))) last = last->*Next; 55 | auto node = _head.load(read_mo); 56 | do { 57 | last->*Next = node; 58 | } while(!_head.compare_exchange_weak(node, first, write_mo, read_mo)); 59 | } 60 | 61 | // A unified interface to get the next element. 62 | inline static T* next(Node *node_or_element) noexcept { 63 | return static_cast(node_or_element->*Next); 64 | } 65 | 66 | // A unified interface to clear the node's metadata. 67 | inline static void clear(Node *node) noexcept { 68 | node->*Next = nullptr; 69 | } 70 | 71 | inline static T* make_fifo(Node *node) noexcept { 72 | if(node == nullptr) return nullptr; 73 | Node sentinel {}; 74 | sentinel.*Next = node; 75 | auto prev = &sentinel, cur = node; 76 | while(cur) { 77 | auto next = cur->*Next; 78 | cur->*Next = prev; 79 | prev = cur; 80 | cur = next; 81 | } 82 | node->*Next = nullptr; 83 | return static_cast(prev); 84 | } 85 | 86 | private: 87 | inline constexpr static auto read_mo = std::memory_order::relaxed; 88 | inline constexpr static auto write_mo = std::memory_order::acq_rel; 89 | alignas(64) std::atomic _head {nullptr}; 90 | }; 91 | 92 | ////////////////////////////////////////////////////////////////////// Multi lock 93 | 94 | // Deprecated. 95 | template 97 | struct multi_lock { 98 | auto fastpath_guard(const std::derived_from auto &stable_object) { 99 | using T = std::decay_t; 100 | auto to_value = std::bit_cast; 101 | auto no_align = [&](auto v) { return v / alignof(T); }; 102 | auto to_index = [&](auto v) { return v % N_way_concurrency; }; 103 | auto then = std::views::transform; 104 | auto view = std::views::single(&stable_object) 105 | | then(to_value) 106 | | then(no_align) 107 | | then(to_index); 108 | return std::unique_lock{_mutexes[view[0]]}; 109 | } 110 | 111 | auto slowpath_guard() { 112 | auto make_scoped_lock = [](auto &&...mutexes) { 113 | return std::scoped_lock{mutexes...}; 114 | }; 115 | return std::apply(make_scoped_lock, _mutexes); 116 | } 117 | 118 | std::array _mutexes; 119 | }; 120 | 121 | ////////////////////////////////////////////////////////////////////// Unified stop source 122 | 123 | 124 | // Make different stop sources more user-facing. 125 | template struct unified_stop_source; 126 | using std_stop_source = unified_stop_source; 127 | using stdexec_stop_source = unified_stop_source; 128 | 129 | template 130 | struct unified_stop_source_base: protected stop_source_impl { 131 | using stop_source_type = self_t; 132 | using underlying_stop_source_type = stop_source_impl; 133 | }; 134 | 135 | template <> 136 | struct unified_stop_source 137 | : protected unified_stop_source_base { 139 | using underlying_stop_source_type::request_stop; 140 | using underlying_stop_source_type::stop_requested; 141 | using underlying_stop_source_type::stop_possible; 142 | auto get_stop_token() const noexcept { return underlying_stop_source_type::get_token(); } 143 | }; 144 | 145 | template <> 146 | struct unified_stop_source 147 | : protected unified_stop_source_base { 149 | using underlying_stop_source_type::request_stop; 150 | using underlying_stop_source_type::stop_requested; 151 | // Associated stop-state is always available in our case. 152 | constexpr auto stop_possible() const noexcept { return true; } 153 | auto get_stop_token() const noexcept { return underlying_stop_source_type::get_token(); } 154 | }; 155 | 156 | ////////////////////////////////////////////////////////////////////// Composable vtable 157 | 158 | struct you_are_a_vtable_signature {}; 159 | 160 | template 161 | concept vtable_signature = requires { 162 | typename Signature::sign_off; 163 | requires std::is_same_v; 165 | }; 166 | 167 | // Per-object vtable for (receiver) type erasure. 168 | // Example: 169 | // using vtable = make_vtable, 170 | // add_complete_to_vtable>; 171 | template 172 | struct make_vtable: Signatures... {}; 173 | 174 | // Make vtable composable. 175 | template 176 | struct add_complete_to_vtable; 177 | 178 | template 179 | struct add_cancel_to_vtable; 180 | 181 | template 182 | struct add_restart_to_vtable; 183 | 184 | template 185 | struct add_complete_to_vtable { 186 | using sign_off = you_are_a_vtable_signature; 187 | Ret (*complete)(Args...) noexcept; 188 | }; 189 | 190 | template 191 | struct add_cancel_to_vtable { 192 | using sign_off = you_are_a_vtable_signature; 193 | Ret (*cancel)(Args...) noexcept; 194 | }; 195 | 196 | template 197 | struct add_restart_to_vtable { 198 | using sign_off = you_are_a_vtable_signature; 199 | Ret (*restart)(Args...) noexcept; 200 | }; 201 | 202 | } // namespace detail 203 | } // namespace uring_exec 204 | -------------------------------------------------------------------------------- /include/uring_exec/io_uring_exec_operation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "io_uring_exec.h" 7 | namespace uring_exec { 8 | 9 | namespace hidden { 10 | 11 | struct nop_io_uring_exec_operation: io_uring_exec::operation_base { 12 | // NOT a real operation state in stdexec. 13 | constexpr nop_io_uring_exec_operation() noexcept 14 | : io_uring_exec::operation_base(this_vtable) {} 15 | inline constexpr static vtable this_vtable { 16 | {.complete = [](auto, auto) noexcept {}}, 17 | {.cancel = [](auto) noexcept {}}, 18 | {.restart = [](auto) noexcept {}}, 19 | }; 20 | }; 21 | 22 | // TODO: constexpr. 23 | inline constinit static nop_io_uring_exec_operation noop; 24 | 25 | } // namespace hidden 26 | 27 | template 28 | struct io_uring_exec_operation: io_uring_exec::operation_base { 29 | using operation_state_concept = stdexec::operation_state_t; 30 | using stop_token_type = stdexec::stop_token_of_t>; 31 | // using stop_callback_type = ...; // See below. 32 | 33 | io_uring_exec_operation(Receiver receiver, 34 | io_uring_exec *uring_control, 35 | std::tuple args) noexcept 36 | : io_uring_exec::operation_base(this_vtable), 37 | receiver(std::move(receiver)), 38 | uring_control(uring_control), 39 | args(std::move(args)) {} 40 | 41 | void start() noexcept { 42 | if(stop_requested()) [[unlikely]] { 43 | stdexec::set_stopped(std::move(receiver)); 44 | return; 45 | } 46 | // NOTE: Don't store the thread_local value to a class member. 47 | // It might be transferred to a different thread. 48 | // See restart()/exec_run::transfer_run() for more details. 49 | auto &local = uring_control->get_local(); 50 | if(auto sqe = io_uring_get_sqe(&local)) [[likely]] { 51 | using op_base = io_uring_exec::operation_base; 52 | local.add_inflight(); 53 | if(false /* temporarily disabled && have_set_stopped() */) { 54 | // Changed to a static noop. 55 | io_uring_sqe_set_data(sqe, static_cast(&hidden::noop)); 56 | io_uring_prep_nop(sqe); 57 | } else { 58 | io_uring_sqe_set_data(sqe, static_cast(this)); 59 | std::apply(F, std::tuple_cat(std::tuple(sqe), std::move(args))); 60 | install_stoppable_callback(local); 61 | } 62 | } else { 63 | // The SQ ring is currently full. 64 | // 65 | // One way to solve this is to make an inline submission here. 66 | // But I don't want to submit operations at separate execution points. 67 | // 68 | // Another solution is to make it never fails by deferred processing. 69 | // TODO: We might need some customization points for minor operations (cancel). 70 | async_restart(); 71 | } 72 | } 73 | 74 | // Since operations are stable (until stdexec::set_xxx(receiver)), 75 | // we can restart again. 76 | void async_restart() noexcept try { 77 | // It can be the same thread, or other threads. 78 | stdexec::scheduler auto scheduler = uring_control->get_scheduler(); 79 | stdexec::sender auto restart_sender = 80 | stdexec::schedule(scheduler) 81 | | stdexec::then([self = this] { 82 | self->start(); 83 | }); 84 | uring_control->get_async_scope().spawn(std::move(restart_sender)); 85 | } catch(...) { 86 | // exec::async_scope.spawn() is throwable. 87 | stdexec::set_error(std::move(receiver), std::current_exception()); 88 | } 89 | 90 | bool stop_requested() noexcept { 91 | // Don't use stdexec::stoppable_token, it has BUG on g++/nvc++. 92 | if constexpr (not stdexec::unstoppable_token) { 93 | auto stop_token = stdexec::get_stop_token(stdexec::get_env(receiver)); 94 | return stop_token.stop_requested(); 95 | } 96 | return false; 97 | } 98 | 99 | void install_stoppable_callback(auto &local) noexcept { 100 | if constexpr (not stdexec::unstoppable_token) { 101 | local_scheduler = local.get_scheduler(); 102 | auto stop_token = stdexec::get_stop_token(stdexec::get_env(receiver)); 103 | stop_callback.emplace(std::move(stop_token), cancellation{this}); 104 | } 105 | } 106 | 107 | // Called by the local thread. 108 | void uninstall_stoppable_callback() noexcept { 109 | if constexpr (not stdexec::unstoppable_token) { 110 | stop_callback.reset(); 111 | auto &q = local_scheduler.context->get_stopping_queue(this); 112 | auto op = q.move_all(); 113 | if(!op) return; 114 | // See the comment in `io_uring_exec_operation_base`. 115 | q.push_all(op, [this](auto node) { return node && node != this; }); 116 | q.push_all(q.next(this), [](auto node) { return node; }); 117 | } 118 | } 119 | 120 | struct cancellation { 121 | // May be called by the requesting thread. 122 | // So we need an atomic operation. 123 | void operator()() noexcept { 124 | auto local = _self->local_scheduler.context; 125 | auto &q = local->get_stopping_queue(_self); 126 | // `q` and `self` are stable. 127 | q.push(_self); 128 | } 129 | io_uring_exec_operation *_self; 130 | }; 131 | 132 | // For stdexec. 133 | using stop_callback_type = typename stop_token_type::template callback_type; 134 | 135 | inline constexpr static vtable this_vtable { 136 | {.complete = [](auto *_self, result_t cqe_res) noexcept { 137 | auto self = static_cast(_self); 138 | auto &receiver = self->receiver; 139 | 140 | self->uninstall_stoppable_callback(); 141 | 142 | constexpr auto is_timer = [] { 143 | // Make GCC happy. 144 | if constexpr (requires { F == &io_uring_prep_timeout; }) 145 | return F == &io_uring_prep_timeout; 146 | return false; 147 | } (); 148 | 149 | // Zero overhead for regular operations. 150 | if constexpr (is_timer) { 151 | auto good = [cqe_res](auto ...errors) { return ((cqe_res == errors) || ...); }; 152 | // Timed out is not an error. 153 | if(good(-ETIME, -ETIMEDOUT)) [[likely]] { 154 | stdexec::set_value(std::move(receiver), cqe_res); 155 | return; 156 | } 157 | } 158 | 159 | if(cqe_res >= 0) [[likely]] { 160 | stdexec::set_value(std::move(receiver), cqe_res); 161 | } else if(cqe_res == -ECANCELED) { 162 | stdexec::set_stopped(std::move(receiver)); 163 | } else { 164 | auto error = std::make_exception_ptr( 165 | std::system_error(-cqe_res, std::system_category())); 166 | stdexec::set_error(std::move(receiver), std::move(error)); 167 | } 168 | }}, 169 | 170 | {.cancel = [](auto *_self) noexcept { 171 | auto self = static_cast(_self); 172 | self->uninstall_stoppable_callback(); 173 | stdexec::set_stopped(std::move(self->receiver)); 174 | }}, 175 | 176 | {.restart = [](auto *_self) noexcept { 177 | auto self = static_cast(_self); 178 | self->uninstall_stoppable_callback(); 179 | self->async_restart(); 180 | }} 181 | }; 182 | 183 | Receiver receiver; 184 | io_uring_exec *uring_control; 185 | std::tuple args; 186 | 187 | io_uring_exec::local_scheduler local_scheduler; 188 | std::optional stop_callback; 189 | }; 190 | 191 | } // namespace uring_exec 192 | -------------------------------------------------------------------------------- /include/uring_exec/io_uring_exec_sender.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include "io_uring_exec.h" 17 | #include "io_uring_exec_operation.h" 18 | namespace uring_exec { 19 | 20 | template 21 | struct io_uring_exec_sender { 22 | using sender_concept = stdexec::sender_t; 23 | using completion_signatures = stdexec::completion_signatures< 24 | stdexec::set_value_t(io_uring_exec::operation_base::result_t), 25 | stdexec::set_error_t(std::exception_ptr), 26 | stdexec::set_stopped_t()>; 27 | 28 | template 29 | io_uring_exec_operation 30 | connect(Receiver receiver) noexcept { 31 | return {std::move(receiver), uring, std::move(args)}; 32 | } 33 | 34 | io_uring_exec *uring; 35 | std::tuple args; 36 | }; 37 | 38 | ////////////////////////////////////////////////////////////////////// Make uring sender 39 | 40 | // Make it easier to create user-defined senders. 41 | template 42 | class make_uring_sender_t { 43 | public: 44 | constexpr 45 | stdexec::sender_of< 46 | stdexec::set_value_t(io_uring_exec::operation_base::result_t /* cqe->res */), 47 | stdexec::set_error_t(std::exception_ptr)> 48 | auto operator()(io_uring_exec::scheduler s, auto &&...args) const noexcept { 49 | return operator()(std::in_place, s, 50 | // Perfectly match the signature. (Arguments are convertible.) 51 | [](R(io_uring_sqe*, Ts...), auto &&...args) { 52 | return std::tuple{static_cast(args)...}; 53 | }(io_uring_prep_invocable, static_cast(args)...)); 54 | } 55 | 56 | private: 57 | template 58 | constexpr auto operator()(std::in_place_t, 59 | io_uring_exec::scheduler s, std::tuple &&t_args) 60 | const noexcept -> io_uring_exec_sender { 61 | return {s.context, std::move(t_args)}; 62 | } 63 | }; 64 | 65 | // A [sender factory] factory for `io_uring_prep_*` asynchronous functions. 66 | // 67 | // These asynchronous senders have similar interfaces to `io_uring_prep_*` functions. 68 | // That is, io_uring_prep_*(sqe, ...) -> async_*(scheduler, ...) 69 | // 70 | // Usage example: 71 | // auto async_close = make_uring_sender_v; 72 | // stdexec::sender auto s = async_close(scheduler, fd); 73 | template 74 | inline constexpr auto make_uring_sender_v = make_uring_sender_t{}; 75 | 76 | ////////////////////////////////////////////////////////////////////// Asynchronous senders 77 | 78 | inline constexpr auto async_oepnat = make_uring_sender_v; 79 | inline constexpr auto async_readv = make_uring_sender_v; 80 | inline constexpr auto async_readv2 = make_uring_sender_v; 81 | inline constexpr auto async_writev = make_uring_sender_v; 82 | inline constexpr auto async_writev2 = make_uring_sender_v; 83 | inline constexpr auto async_close = make_uring_sender_v; 84 | inline constexpr auto async_socket = make_uring_sender_v; 85 | inline constexpr auto async_bind = make_uring_sender_v; 86 | inline constexpr auto async_accept = make_uring_sender_v; 87 | inline constexpr auto async_connect = make_uring_sender_v; 88 | inline constexpr auto async_send = make_uring_sender_v; 89 | inline constexpr auto async_recv = make_uring_sender_v; 90 | inline constexpr auto async_sendmsg = make_uring_sender_v; 91 | inline constexpr auto async_recvmsg = make_uring_sender_v; 92 | inline constexpr auto async_shutdown = make_uring_sender_v; 93 | inline constexpr auto async_poll_add = make_uring_sender_v; 94 | inline constexpr auto async_poll_update = make_uring_sender_v; 95 | inline constexpr auto async_poll_remove = make_uring_sender_v; 96 | inline constexpr auto async_timeout = make_uring_sender_v; 97 | inline constexpr auto async_futex_wait = make_uring_sender_v; 98 | inline constexpr auto async_futex_wake = make_uring_sender_v; 99 | 100 | // Debug only. (For example, to verify the correctness of concurrency.) 101 | // The return value makes no sense. 102 | inline constexpr auto async_nop = make_uring_sender_v; 103 | 104 | // `async_open` needs a new version of liburing: https://github.com/axboe/liburing/issues/1100 105 | // inline constexpr auto async_open = make_uring_sender_v; 106 | 107 | // On files that support seeking, if the `offset` is set to -1, the read operation commences at the file offset, 108 | // and the file offset is incremented by the number of bytes read. See read(2) for more details. Note that for an 109 | // async API, reading and updating the current file offset may result in unpredictable behavior, unless access to 110 | // the file is serialized. It is **not encouraged** to use this feature, if it's possible to provide the desired IO 111 | // offset from the application or library. 112 | inline constexpr stdexec::sender 113 | auto async_read(io_uring_exec::scheduler s, int fd, void *buf, size_t n, uint64_t offset = 0) noexcept { 114 | return make_uring_sender_v(s, fd, buf, n, offset); 115 | } 116 | 117 | inline constexpr stdexec::sender 118 | auto async_write(io_uring_exec::scheduler s, int fd, const void *buf, size_t n, uint64_t offset = 0) noexcept { 119 | return make_uring_sender_v(s, fd, buf, n, offset); 120 | } 121 | 122 | inline stdexec::sender 123 | auto async_wait(io_uring_exec::scheduler s, std::chrono::steady_clock::duration duration) noexcept { 124 | using namespace std::chrono; 125 | auto make_ts_from = [](auto duration) -> struct __kernel_timespec { 126 | auto duration_s = duration_cast(duration); 127 | auto duration_ns = duration_cast(duration - duration_s); 128 | return {.tv_sec = duration_s.count(), .tv_nsec = duration_ns.count()}; 129 | }; 130 | // `ts` needs safe lifetime within an asynchronous scope. 131 | return stdexec::let_value(stdexec::just(make_ts_from(duration)), [s](auto &&ts) { 132 | return make_uring_sender_v(s, &ts, 0, 0); 133 | }); 134 | } 135 | 136 | inline stdexec::sender 137 | auto async_sigwait(io_uring_exec::scheduler scheduler, std::ranges::range auto signals) noexcept { 138 | struct raii_fd { 139 | int fd; 140 | explicit raii_fd(int fd) noexcept: fd(fd) {} 141 | raii_fd(raii_fd &&rhs) noexcept: fd(rhs.reset()) {} 142 | ~raii_fd() { if(fd > -1) close(fd); } 143 | int reset() noexcept { return std::exchange(fd, -1); } 144 | }; 145 | auto make_signalfd_from = [](auto signals) { 146 | sigset_t mask; 147 | sigemptyset(&mask); 148 | for(auto signal : signals) sigaddset(&mask, signal); 149 | pthread_sigmask(SIG_BLOCK, &mask, nullptr); 150 | return raii_fd{signalfd(-1, &mask, 0)}; 151 | }; 152 | return 153 | stdexec::let_value(stdexec::just(make_signalfd_from(std::move(signals))), [=](auto &fd) { 154 | // Propagate to upon_error/let_error. 155 | if(fd.fd < 0) throw std::system_error(errno, std::system_category()); 156 | return 157 | stdexec::let_value(stdexec::just(fd.fd), [=](auto fd) { 158 | // NOTE: 159 | // Since signalfd has no ownership of signal, async_poll_add & async_close are incorrect. 160 | // We must async_read the signal. Otherwise signal will wakeup another signalfd. 161 | return async_poll_add(scheduler, fd, POLLIN); 162 | }) 163 | | stdexec::let_value([=](auto events) { 164 | // FIXME: POLLERR differs from cqe_res < 0. But what is the undocumented difference? 165 | if(!(events & POLLIN)) throw std::system_error(errno, std::system_category()); 166 | return stdexec::just(); 167 | }) 168 | | stdexec::let_value([=, &fd] { 169 | return 170 | stdexec::just(fd.fd, std::array()) 171 | | stdexec::let_value([=](auto fd, auto &buf) { 172 | return 173 | async_read(scheduler, fd, buf.data(), buf.size()) 174 | | stdexec::then([&buf](auto) { 175 | // See `man 2 signalfd` for more details. 176 | return std::bit_cast(buf); 177 | }); 178 | }); 179 | }); 180 | }); 181 | } 182 | 183 | } // namespace uring_exec 184 | -------------------------------------------------------------------------------- /include/uring_exec/io_uring_exec_internal.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "detail.h" 13 | #include "io_uring_exec_internal_run.h" 14 | #include "underlying_io_uring.h" 15 | namespace uring_exec { 16 | namespace internal { 17 | 18 | ////////////////////////////////////////////////////////////////////// Containers 19 | 20 | // Avoid requiring a default constructor in derived classes. 21 | struct intrusive_node { 22 | intrusive_node *_i_next {nullptr}; 23 | }; 24 | 25 | // Simple fixed-size map to reduce thread contention. 26 | template 27 | struct trivial_intrusive_map: detail::immovable { 28 | using bucket_type = Bucket; 29 | using element_type = typename Bucket::element_type; 30 | 31 | auto& operator[](size_t index) noexcept { return map[index]; } 32 | auto& operator[](element_type *element) noexcept { 33 | // Elements have stable addresses and can locate their queue using their own address. 34 | static_assert( 35 | !std::is_move_constructible_v && 36 | !std::is_move_assignable_v); 37 | auto index = std::bit_cast(element) % N_way_concurrency; 38 | return map[index]; 39 | } 40 | 41 | struct robin_access_pattern { 42 | trivial_intrusive_map &self; 43 | auto& operator[](size_t index) const noexcept { return self[index % Fixed_bucket_size]; }; 44 | }; 45 | 46 | struct random_access_pattern { 47 | trivial_intrusive_map &self; 48 | auto& operator[](size_t) const noexcept { return self[mt_random()]; }; 49 | 50 | // Random device may perform an open() syscall and fd won't be closed until dtor is called. 51 | // Therefore, we don't cache it. 52 | inline static thread_local auto mt_random = 53 | [algo = std::mt19937_64{std::random_device{}()}, 54 | dist = std::uniform_int_distribution{0, Fixed_bucket_size - 1}]() mutable 55 | { 56 | static_assert(Fixed_bucket_size > 0, "Zero bucket design is not allowed."); 57 | return dist(algo); 58 | }; 59 | }; 60 | 61 | auto robin_access() noexcept -> robin_access_pattern { return {*this}; }; 62 | auto random_access() noexcept -> random_access_pattern { return {*this}; } 63 | 64 | inline constexpr static size_t N_way_concurrency = Fixed_bucket_size; 65 | 66 | private: 67 | std::array map; 68 | }; 69 | 70 | ////////////////////////////////////////////////////////////////////// Task support 71 | 72 | // All the tasks are asynchronous. 73 | // The `io_uring_exec_task` struct is queued by a user-space intrusive queue. 74 | // NOTE: The io_uring-specified task is queued by an interal ring of io_uring. 75 | struct io_uring_exec_task: detail::immovable, intrusive_node { 76 | using vtable = detail::make_vtable< 77 | detail::add_complete_to_vtable, 78 | detail::add_cancel_to_vtable >; 79 | io_uring_exec_task(vtable vtab) noexcept: vtab(vtab) {} 80 | // Receiver types erased. 81 | vtable vtab; 82 | }; 83 | 84 | // Atomic version. 85 | using intrusive_task_queue = detail::intrusive_queue; 86 | 87 | // More concurrency-friendly. 88 | using intrusive_task_map = trivial_intrusive_map; 89 | 90 | // `internal::start_operation` is a customization point for `trivial_scheduler` template. 91 | inline void start_operation(intrusive_task_queue *self, auto *operation) noexcept { 92 | self->push(operation); 93 | } 94 | 95 | ////////////////////////////////////////////////////////////////////// io_uring async operations 96 | 97 | // For (personal) debugging purpose, make it a different type from `intrusive_node`. 98 | struct intrusive_node_with_meta { 99 | intrusive_node_with_meta *_i_m_next {nullptr}; 100 | 101 | #ifdef URING_EXEC_DEBUG 102 | void *_for_gdb {}; 103 | #endif 104 | }; 105 | 106 | // External structured callbacks support. 107 | // See io_uring_exec_operation.h and io_uring_exec_sender.h for more details. 108 | struct io_uring_exec_operation_base: detail::immovable, intrusive_node_with_meta { 109 | using result_t = decltype(std::declval().res); 110 | using _self_t = io_uring_exec_operation_base; 111 | using vtable = detail::make_vtable< 112 | detail::add_complete_to_vtable, 113 | detail::add_cancel_to_vtable , 114 | detail::add_restart_to_vtable >; 115 | constexpr io_uring_exec_operation_base(vtable vtab) noexcept: vtab(vtab) {} 116 | vtable vtab; 117 | // We might need a bidirectional intrusive node for early stopping support. 118 | // However, this feature is rarely used in practice. 119 | // We can use hashmap instead to avoid wasting footprint for every object's memory layout. 120 | }; 121 | 122 | using intrusive_operation_queue = detail::intrusive_queue< 123 | io_uring_exec_operation_base, 124 | &io_uring_exec_operation_base::_i_m_next>; 125 | 126 | using intrusive_operation_map = trivial_intrusive_map; 127 | 128 | ////////////////////////////////////////////////////////////////////// stdexec scheduler template 129 | 130 | // For `stdexec::scheduler` concept. 131 | // We might have several scheduler implementations, so make a simple template for them. 132 | template 133 | struct trivial_scheduler { 134 | template 135 | struct operation: io_uring_exec_task { 136 | using operation_state_concept = stdexec::operation_state_t; 137 | 138 | Receiver receiver; 139 | Context *context; 140 | 141 | void start() noexcept { 142 | // Private customization point. 143 | start_operation(context, this); 144 | } 145 | 146 | inline constexpr static vtable this_vtable { 147 | {.complete = [](io_uring_exec_task *_self) noexcept { 148 | auto &receiver = static_cast(_self)->receiver; 149 | using env_type = stdexec::env_of_t; 150 | using stop_token_type = stdexec::stop_token_of_t; 151 | if constexpr (stdexec::unstoppable_token) { 152 | stdexec::set_value(std::move(receiver)); 153 | return; 154 | } 155 | auto stop_token = stdexec::get_stop_token(stdexec::get_env(receiver)); 156 | stop_token.stop_requested() ? 157 | stdexec::set_stopped(std::move(receiver)) 158 | : stdexec::set_value(std::move(receiver)); 159 | }}, 160 | {.cancel = [](io_uring_exec_task *_self) noexcept { 161 | auto self = static_cast(_self); 162 | stdexec::set_stopped(std::move(self->receiver)); 163 | }} 164 | }; 165 | }; 166 | struct sender { 167 | using sender_concept = stdexec::sender_t; 168 | using completion_signatures = stdexec::completion_signatures< 169 | stdexec::set_value_t(), 170 | stdexec::set_stopped_t()>; 171 | struct env { 172 | template 173 | auto query(stdexec::get_completion_scheduler_t) const noexcept { 174 | return trivial_scheduler{context}; 175 | } 176 | Context *context; 177 | }; 178 | 179 | env get_env() const noexcept { return {context}; } 180 | 181 | template 182 | operation connect(Receiver receiver) noexcept { 183 | return {{operation::this_vtable}, std::move(receiver), context}; 184 | } 185 | 186 | Context *context; 187 | }; 188 | bool operator<=>(const trivial_scheduler &) const=default; 189 | sender schedule() const noexcept { return {context}; } 190 | 191 | Context *context; 192 | }; 193 | 194 | ////////////////////////////////////////////////////////////////////// Local side 195 | 196 | class io_uring_exec; 197 | 198 | // Is-a runnable io_uring. 199 | class io_uring_exec_local: public underlying_io_uring, 200 | public io_uring_exec_run 202 | { 203 | public: 204 | io_uring_exec_local(constructor_parameters p, 205 | io_uring_exec &root); 206 | 207 | ~io_uring_exec_local(); 208 | 209 | using io_uring_exec_run::run_policy; 210 | using io_uring_exec_run::run; 211 | 212 | auto& get_remote() noexcept { return _root; } 213 | auto& get_local() noexcept { return *this; } 214 | 215 | auto& get_async_scope() noexcept { return _local_scope; } 216 | 217 | using scheduler = trivial_scheduler; 218 | scheduler get_scheduler() noexcept { return {this}; } 219 | 220 | void add_inflight() noexcept { _inflight++; } 221 | void remove_inflight() noexcept { _inflight--; } 222 | 223 | decltype(auto) get_stopping_queue(io_uring_exec_operation_base *op) noexcept { 224 | return _stopping_map[op]; 225 | } 226 | 227 | // Hidden friends. 228 | private: 229 | friend void start_operation(io_uring_exec_local *self, auto *operation) noexcept { 230 | self->_attached_queue.push(operation); 231 | } 232 | 233 | template 234 | friend struct io_uring_exec_run; 235 | friend class io_uring_exec; 236 | 237 | private: 238 | // This is a MPSC queue, while remote queue is a MPMC queue. 239 | // It can help scheduler attach to a specified thread (C). 240 | intrusive_task_queue _attached_queue; 241 | intrusive_operation_map _stopping_map; 242 | size_t _inflight {}; 243 | exec::async_scope _local_scope; 244 | io_uring_exec &_root; 245 | std::thread::id _root_tid; 246 | }; 247 | 248 | ////////////////////////////////////////////////////////////////////// control block 249 | 250 | class io_uring_exec: public underlying_io_uring, // For IORING_SETUP_ATTACH_WQ. 251 | public io_uring_exec_run, 252 | private detail::unified_stop_source 253 | { 254 | public: 255 | // Example: io_uring_exec uring({.uring_entries=512}); 256 | template // For per-object-thread_local dispatch in compile time. 257 | io_uring_exec(underlying_io_uring::constructor_parameters params) noexcept 258 | : underlying_io_uring(params), _uring_params(params), 259 | _thread_id(std::this_thread::get_id()), 260 | _remote_handle{io_uring_exec_remote_handle::this_vtable} 261 | { 262 | // Then it will broadcast to thread-local urings. 263 | params.ring_fd = this->ring_fd; 264 | } 265 | 266 | template // MUST declare. 267 | io_uring_exec(unsigned uring_entries, int uring_flags = 0) noexcept 268 | : io_uring_exec({.uring_entries = uring_entries, .uring_flags = uring_flags}) {} 269 | 270 | ~io_uring_exec() { 271 | // For on-stack uring.run(). 272 | io_uring_exec_run::transfer_run(); 273 | // Cancel all the pending tasks/operations. 274 | io_uring_exec_run::terminal_run(); 275 | } 276 | 277 | // NOTE: Assumed that you're most likely using a singleton pattern. (e.g., async_main()) 278 | // However, in some use cases, it can also be a per-object `get_local()`. 279 | // 280 | // Example 1 (Good): 281 | // io_uring_exec a {...}; 282 | // io_uring_exec b {...}; 283 | // assert(&a.get_local() != &b.get_local()); 284 | // 285 | // Example 2 (Bad): 286 | // for(auto step : iota(1)) 287 | // io_uring_exec c {...}; 288 | // assert(&c.get_local() == &c.get_local()); 289 | // 290 | // Example 2 can be fixed by a more complex trick without runtime mapping. 291 | // Although the overhead is low (rebind `_root`), I don't plan to support it. 292 | auto get_local() noexcept -> io_uring_exec_local& { return _remote_handle.vtab.complete(*this); } 293 | auto get_remote() noexcept -> io_uring_exec& { return *this; } 294 | 295 | using task = internal::io_uring_exec_task; 296 | using operation_base = internal::io_uring_exec_operation_base; 297 | using task_queue = internal::intrusive_task_queue; 298 | using task_map = internal::intrusive_task_map; 299 | 300 | // Required by stdexec. 301 | // Most of its functions are invoked by stdexec. 302 | using scheduler = trivial_scheduler; 303 | using local_scheduler = io_uring_exec_local::scheduler; 304 | 305 | auto get_scheduler() noexcept { return scheduler{this}; } 306 | auto get_local_scheduler() noexcept { return get_local().get_scheduler(); } 307 | auto get_local_scheduler(std::thread::id) = delete; // TODO 308 | 309 | // Run with customizable policy. 310 | // 311 | // If you want to change a few options based on a default config, try this way: 312 | // ``` 313 | // constexpr auto policy = [] { 314 | // auto policy = io_uring_exec::run_policy{}; 315 | // policy.launch = false; 316 | // policy.realtime = true; 317 | // // ... 318 | // return policy; 319 | // } (); 320 | // uring.run(); 321 | // ``` 322 | // 323 | // If you want to change only one option, try this way: 324 | // ``` 325 | // constexpr io_uring_exec::run_policy policy {.busyloop = true}; 326 | // ``` 327 | // NOTE: Designated initializers cannot be reordered. 328 | using io_uring_exec_run::run_policy; 329 | using io_uring_exec_run::run; 330 | 331 | using stop_source_type::underlying_stop_source_type; 332 | using stop_source_type::request_stop; 333 | using stop_source_type::stop_requested; 334 | using stop_source_type::stop_possible; 335 | using stop_source_type::get_stop_token; 336 | // No effect. 337 | // Just remind you that it differs from the C++ standard. 338 | auto get_token() = delete; 339 | 340 | auto get_async_scope() noexcept -> exec::async_scope& { return _transfer_scope; } 341 | 342 | // Hidden friends. 343 | private: 344 | template 345 | friend struct io_uring_exec_run; 346 | friend class io_uring_exec_local; 347 | 348 | friend void start_operation(io_uring_exec *self, auto *operation) noexcept { 349 | // Break self-indexing rule as we don't need to find map bucket by operation itself. 350 | auto index = self->_store_balance.fetch_add(1, std::memory_order::relaxed); 351 | self->_immediate_map.robin_access()[index].push(operation); 352 | } 353 | 354 | private: 355 | underlying_io_uring::constructor_parameters _uring_params; 356 | intrusive_task_map _immediate_map; 357 | alignas(64) std::atomic _store_balance {}; 358 | alignas(64) std::atomic _running_local {}; 359 | exec::async_scope _transfer_scope; 360 | std::thread::id _thread_id; 361 | 362 | private: 363 | // This makes per-io_uring_exec.get_local() possible 364 | // in certain use cases. (NOT all use cases.) 365 | struct io_uring_exec_remote_handle { 366 | using vtable = detail::make_vtable< 367 | detail::add_complete_to_vtable< 368 | io_uring_exec_local&(io_uring_exec&)>>; 369 | const vtable vtab; 370 | 371 | template 372 | inline constexpr static vtable this_vtable = { 373 | {.complete = [](auto &self) noexcept -> io_uring_exec_local& { 374 | // Note that we use thread storage duration for `local`. 375 | // Therefore, we need to make them served by different types. 376 | thread_local io_uring_exec_local local(self._uring_params, self); 377 | return local; 378 | }} 379 | }; 380 | } _remote_handle; 381 | 382 | }; 383 | 384 | ////////////////////////////////////////////////////////////////////// misc 385 | 386 | inline 387 | io_uring_exec_local::io_uring_exec_local( 388 | io_uring_exec_local::constructor_parameters p, 389 | io_uring_exec &root) 390 | : underlying_io_uring(p), 391 | _root(root), 392 | _root_tid(root._thread_id) 393 | { 394 | _root._running_local.fetch_add(1, std::memory_order::relaxed); 395 | } 396 | 397 | // There may be a UB if `_root` is accessed unconditionally, 398 | // since thread storage duration in main thread is longer than remote exec. 399 | // (Other threads are fine to do so.) 400 | // But no compiler can detect and reproduce this problem. 401 | // In any case, we need a local tid to perform the check. 402 | inline 403 | io_uring_exec_local::~io_uring_exec_local() { 404 | io_uring_exec_run::transfer_run(); 405 | thread_local auto tid = std::this_thread::get_id(); 406 | if(tid == _root_tid) return; 407 | _root._running_local.fetch_sub(1, std::memory_order::acq_rel); 408 | } 409 | 410 | } // namespace internal 411 | } // namespace uring_exec 412 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /include/uring_exec/io_uring_exec_internal_run.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "underlying_io_uring.h" 15 | namespace uring_exec { 16 | namespace internal { 17 | 18 | class io_uring_exec_local; 19 | 20 | // CRTP type for `io_uring_exec_local` and `io_uring_exec`. 21 | template 23 | struct io_uring_exec_run { 24 | struct run_policy { 25 | // Informal forward progress guarantees. 26 | // NOTES: 27 | // * These are exclusive flags, but using bool (not enum) for simplification. 28 | // * `weakly_concurrent` is not a C++ standard part, which can make progress eventually 29 | // with lower overhead compared to `concurrent`, provided it is used properly. 30 | // * `parallel` (which makes progress per `step`) is NOT supported for IO operations. 31 | bool concurrent {true}; // which requires that a thread makes progress eventually. 32 | bool weakly_parallel {false}; // which does not require that the thread makes progress. 33 | bool weakly_concurrent {false}; // which requires that a thread may make progress eventually. 34 | 35 | // Event handling. 36 | // Any combination is welcome. 37 | bool launch {true}; 38 | bool submit {true}; 39 | bool iodone {true}; 40 | 41 | // Behavior details. 42 | bool busyloop {false}; // No yield. 43 | bool autoquit {false}; // `concurrent` runs infinitely by default. 44 | bool realtime {false}; // (DEPRECATED) No deferred processing. 45 | bool waitable {false}; // Submit and wait. 46 | bool hookable {true}; // Always true beacause of per-object vtable. 47 | bool detached {false}; // Ignore stop requests from `io_uring_exec`. 48 | bool progress {false}; // run() returns run_progress_info. 49 | bool no_delay {false}; // Complete I/O as fast as possible. 50 | bool pull_all {false}; // (WIP) Issue I/O as fast as possible. 51 | bool blocking {false}; // in-flight operations cannot be interrupted by a stop request. 52 | bool locality {false}; // Queue tasks in FILO order. 53 | 54 | bool transfer {false}; // For stopeed local context. Just a tricky restart. 55 | bool terminal {false}; // For stopped remote context. Cancel All. 56 | 57 | size_t iodone_batch {64}; // (Roughly estimated value) for `io_uring_peek_batch_cqe`. 58 | size_t iodone_maxnr {512}; // Maximum number of `cqe`s that can be taken in one step. 59 | }; 60 | 61 | // Tell the compiler we're not using the return value. 62 | struct run_progress_no_info { 63 | decltype(std::ignore) _1, _2, _3, _4; 64 | void operator()(...) const noexcept {} 65 | void operator+=(const auto &) const noexcept {} 66 | }; 67 | 68 | struct run_progress_info { 69 | size_t loop_step {}; 70 | size_t launched {}; // For intrusive task queue. 71 | size_t submitted {}; // For `io_uring_submit`. 72 | size_t done {}; // For `io_uring_peek_batch_cqe`. 73 | 74 | auto operator()(size_t final_step) noexcept { 75 | loop_step = final_step; 76 | return *this; 77 | } 78 | 79 | // Vertically sum all the class members. 80 | auto& operator+=(const run_progress_info &rhs) noexcept { 81 | // Don't worry about the codegen performance, 82 | // it is as efficient as manually summing each member by name. 83 | auto &lhs = *this; 84 | using strict_alias = std::array; 85 | auto l = std::bit_cast(lhs); 86 | auto r = std::bit_cast(rhs); 87 | std::ranges::transform(l, r, begin(l), std::plus()); 88 | return lhs = std::bit_cast(l); 89 | } 90 | 91 | template 92 | inline constexpr static auto make() { 93 | if constexpr (really_need) { 94 | return run_progress_info(); 95 | } else { 96 | return run_progress_no_info(); 97 | } 98 | } 99 | }; 100 | 101 | // run_policy: See the comments above. 102 | // any_stop_token_t: Compatible with `std::jthread` and `std::stop_token` for a unified interface. 103 | // Return type: Either `run_progress_info` or `void`, depending on `run_policy.progress`. 104 | template 106 | auto run(any_stop_token_t external_stop_token = {}) { 107 | constexpr auto sum_options = [](auto ...options) { 108 | return (int{options} + ...); 109 | }; 110 | static_assert( 111 | sum_options(policy.concurrent, 112 | policy.weakly_parallel, 113 | policy.weakly_concurrent) 114 | == 1, 115 | "Small. Fast. Reliable. Choose any three." 116 | ); 117 | 118 | // Progress, and the return value of run(). 119 | 120 | constexpr bool any_progress_possible = 121 | sum_options(policy.launch, policy.submit, policy.iodone); 122 | 123 | auto progress_info = run_progress_info::template make(); 124 | auto progress_info_one_step = run_progress_info::template make(); 125 | auto &&[_, launched, submitted, done] = progress_info_one_step; 126 | 127 | auto &remote = that()->get_remote(); 128 | auto &local = that()->get_local(); 129 | 130 | if(local._runloop_must_have_stopped) [[unlikely]] { 131 | return progress_info(0); 132 | } 133 | 134 | // We don't need this legacy way. 135 | // It was originally designed to work with a single std::stop_token type, 136 | // and thus requires a runtime check for a unified (non-void, but won't stop) interface. 137 | // 138 | // Instead, use `stdexec::never_stop_token` for a more constexpr-friendly way. 139 | // We can infer the compile-time information from its type. 140 | // 141 | // If a type other than never_stop_token is passed to this function, 142 | // we assume that it must be `stop_possible() == true`. 143 | // This allow us to check stop_requested directly, 144 | // and reduce at least one trivial operation. 145 | // 146 | // auto legacy_stop_requested = 147 | // [&, possible = external_stop_token.stop_possible()] { 148 | // if(!possible) return false; 149 | // return external_stop_token.stop_requested(); 150 | // }; 151 | 152 | // Return `step` as a performance hint. 153 | // 0 means no-op. 154 | for(size_t step = 1; ; _walltime_step++, step++) { 155 | if constexpr (policy.launch) { 156 | // TODO: Lockfree wrapper for auto std::move(q) -> op*. 157 | auto launch = [&launched](auto &intrusive_queue) { 158 | auto &q = intrusive_queue; 159 | auto op = q.move_all(); 160 | if constexpr (not policy.locality) { 161 | op = q.make_fifo(op); 162 | } 163 | // NOTE: 164 | // We need to get the `next(op)` first. 165 | // Because `op` will be destroyed after complete/cancel(). 166 | auto safe_for_each = [&q, op](auto &&f) mutable { 167 | // It won't modify the outer `op`. 168 | // If we need any later operation on it. 169 | for(; op; f(std::exchange(op, q.next(op)))); 170 | }; 171 | safe_for_each([&launched](auto op) { 172 | if constexpr (policy.terminal) { 173 | op->vtab.cancel(op); 174 | // Make Clang happy. 175 | (void)launched; 176 | } else { 177 | op->vtab.complete(op); 178 | launched++; 179 | } 180 | }); 181 | // TODO: record the first task (op). 182 | // Used to detect whether it is on-stack or on-heap 183 | // as a performance hint. 184 | }; 185 | // No need to pull remote tasks. 186 | if constexpr (policy.transfer) { 187 | launch(local._attached_queue); 188 | // Pull any two remote task queues. 189 | } else { 190 | launch(local._attached_queue); 191 | launch(remote._immediate_map.robin_access()[_walltime_step]); 192 | launch(remote._immediate_map.random_access()[_walltime_step]); 193 | } 194 | } 195 | 196 | if constexpr (policy.submit) { 197 | // TODO: wait_{one|some|all}. 198 | if constexpr (policy.waitable) { 199 | submitted = io_uring_submit_and_wait(&local, 1); 200 | } else { 201 | submitted = io_uring_submit(&local); 202 | } 203 | } 204 | 205 | if constexpr (policy.iodone) { 206 | std::array cqes; 207 | auto produce_some = [&] { 208 | return io_uring_peek_batch_cqe( 209 | &local, cqes.data(), cqes.size()); 210 | }; 211 | auto consume_one = [&](io_uring_cqe* cqe) { 212 | auto user_data = io_uring_cqe_get_data(cqe); 213 | using uop = io_uring_exec_operation_base; 214 | auto uring_op = std::bit_cast(user_data); 215 | if constexpr (policy.transfer) { 216 | uring_op->vtab.restart(uring_op); 217 | } else { 218 | uring_op->vtab.complete(uring_op, cqe->res); 219 | } 220 | }; 221 | auto consume_some = [&](std::ranges::view auto some_view) { 222 | for(auto one : some_view) consume_one(one); 223 | }; 224 | auto greedy = [&](auto some) { 225 | // Shut up and take all! 226 | if constexpr (policy.transfer || policy.terminal) return false; 227 | // Take too many cqes in this step. 228 | // Retry in the next step to prevent .launch/.submit starvation. 229 | if(done > policy.iodone_maxnr) return true; 230 | // Not full. There may be short I/Os. 231 | // It can take more, but not much benefit. 232 | // NOTE: It is not recommended to move this line up. 233 | // Since our intrusive queues are FILO design. 234 | if(some != cqes.size()) return not policy.no_delay; 235 | return false; 236 | }; 237 | for(unsigned some; (some = produce_some()) > 0;) { 238 | consume_some(cqes | std::views::take(some)); 239 | io_uring_cq_advance(&local, some); 240 | done += some; 241 | local._inflight -= some; 242 | if(greedy(some)) break; 243 | } 244 | } 245 | 246 | // Not accounted in progress info. 247 | if constexpr (not policy.blocking) { 248 | // No need to traverse the map. 249 | auto &q = local._stopping_map.robin_access()[_walltime_step]; 250 | // No need to use `safe_for_each`. 251 | for(auto op = q.move_all(); op;) { 252 | auto sqe = io_uring_get_sqe(&local); 253 | // `uring` is currently full. Retry in the next round. 254 | if(!sqe) [[unlikely]] { 255 | q.push_all(op, [](auto node) { return node; }); 256 | break; 257 | } 258 | io_uring_sqe_set_data(sqe, &noop); 259 | io_uring_prep_cancel(sqe, op, {}); 260 | local.add_inflight(); 261 | q.clear(std::exchange(op, q.next(op))); 262 | } 263 | } 264 | 265 | if constexpr (policy.weakly_parallel) { 266 | return progress_info(step); 267 | } 268 | 269 | bool any_progress = false; 270 | if constexpr (any_progress_possible) { 271 | any_progress |= bool(launched); 272 | any_progress |= bool(submitted); 273 | any_progress |= bool(done); 274 | progress_info += progress_info_one_step; 275 | progress_info_one_step = {}; 276 | } 277 | 278 | if constexpr (policy.weakly_concurrent) { 279 | if(any_progress) return progress_info(step); 280 | } 281 | 282 | // Per-run() stop token. 283 | // 284 | // We use `stdexec::never_stop_token` by default. 285 | // Its `stop_requested()` returns false in a constexpr way. 286 | // So we don't need to add another detached policy here. 287 | if(external_stop_token.stop_requested()) { 288 | return progress_info(step); 289 | } 290 | 291 | // Ignore the context's stop request can help reduce at least one atomic operation. 292 | // This might be useful for some network I/O patterns. 293 | if constexpr (not policy.detached) { 294 | if(remote.stop_requested()) { 295 | // A weakly_parallel work. Won't check the request recursively. 296 | inplace_terminal_run(local); 297 | // This local runloop is not allowed to run again. 298 | local._runloop_must_have_stopped = true; 299 | return progress_info(step); 300 | } 301 | } 302 | 303 | if constexpr (policy.autoquit) { 304 | if(!local._inflight) { 305 | return progress_info(step); 306 | } 307 | } 308 | 309 | if constexpr (not policy.busyloop) { 310 | if(!any_progress) { 311 | std::this_thread::yield(); 312 | } 313 | } 314 | } 315 | return progress_info(0); 316 | } 317 | 318 | protected: 319 | void transfer_run() { 320 | auto &local = that()->get_local(); 321 | submit_destructive_command(local); 322 | constexpr auto policy = [] { 323 | auto policy = run_policy{}; 324 | policy.concurrent = false; 325 | policy.weakly_parallel = true; 326 | policy.transfer = true; 327 | return policy; 328 | } (); 329 | run(); 330 | } 331 | 332 | void terminal_run() { 333 | auto &main_local = that()->get_local(); 334 | auto &remote = that()->get_remote(); 335 | // Exit run() and transfer. 336 | remote.request_stop(); 337 | // TODO: latch or std::atomic::wait(). 338 | // Main thread == 1. 339 | while(remote._running_local.load(std::memory_order::acquire) > 1) { 340 | std::this_thread::yield(); 341 | } 342 | inplace_terminal_run(main_local); 343 | } 344 | 345 | void inplace_terminal_run(auto &local) { 346 | submit_destructive_command(local); 347 | constexpr auto policy = [] { 348 | auto policy = run_policy{}; 349 | policy.concurrent = false; 350 | policy.weakly_parallel = true; 351 | policy.terminal = true; 352 | return policy; 353 | } (); 354 | run(); 355 | } 356 | 357 | protected: 358 | // `std::false_type` has no viable overloaded '=' but initializable. 359 | struct _invalid_bool: std::false_type { operator bool() = delete; }; 360 | 361 | // A restricted boolean type. Must be called by `local.`. 362 | // TODO: use EBO instead. 363 | using _local_specified_bool = 364 | std::conditional_t< 365 | std::is_same_v, 366 | bool, _invalid_bool>; 367 | 368 | // Avoid atomic stop_requested() operation. 369 | _local_specified_bool _runloop_must_have_stopped {}; 370 | 371 | private: 372 | size_t _walltime_step {std::hash{}(std::this_thread::get_id())}; 373 | 374 | constexpr auto that() noexcept -> Exec_crtp_derived* { 375 | return static_cast(this); 376 | } 377 | 378 | // liburing has different types between cqe->`user_data` and set_data[64](`user_data`). 379 | // Don't know why. 380 | using unified_user_data_type = decltype( 381 | [](R(io_uring_sqe*, T)) { 382 | return T{}; 383 | } (io_uring_sqe_set_data)); 384 | 385 | [[deprecated("destructive_command is now a nop operation.")]] 386 | constexpr auto make_destructive_command() noexcept { 387 | // Impossible address for Linux user space. 388 | auto impossible = std::numeric_limits::max(); 389 | // Don't care about whether it is a value or a pointer. 390 | return std::bit_cast(impossible); 391 | } 392 | 393 | [[deprecated("destructive_command is now a nop operation.")]] 394 | constexpr auto test_destructive_command(unified_user_data_type user_data) noexcept { 395 | return make_destructive_command() == user_data; 396 | } 397 | 398 | void submit_destructive_command(underlying_io_uring &uring) noexcept { 399 | // Flush, and ensure that the cancel-sqe must be allocated successfully. 400 | io_uring_submit(&uring); 401 | auto sqe = io_uring_get_sqe(&uring); 402 | // `noop` is an object with static storage duration. 403 | // It makes no effect, but it can reduce if-statement branches. 404 | io_uring_sqe_set_data(sqe, &noop); 405 | io_uring_prep_cancel(sqe, {}, IORING_ASYNC_CANCEL_ANY); 406 | io_uring_submit(&uring); 407 | } 408 | 409 | inline constexpr static io_uring_exec_operation_base::vtable noop_vtable { 410 | {.complete = [](auto, auto) noexcept {}}, 411 | {.cancel = [](auto) noexcept {}}, 412 | {.restart = [](auto) noexcept {}}, 413 | }; 414 | 415 | inline constinit static io_uring_exec_operation_base noop {noop_vtable}; 416 | }; 417 | 418 | } // namespace internal 419 | } // namespace uring_exec 420 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uring_exec 2 | 3 | [适配 io_uring 异步函数到 C++26 std::execution](https://www.bluepuni.com/archives/porting-liburing-to-stdexec/) 4 | 5 | ## Introduction 6 | 7 | This project attempts to provide [`stdexec`](https://github.com/NVIDIA/stdexec) support for [`liburing`](https://github.com/axboe/liburing). It is also a `std::execution`-based network library. 8 | 9 | ## Features 10 | 11 | + [sender/receiver](#senderreceiver) 12 | + [C++20 coroutine](#c20-coroutine) 13 | + [Thread pool](#thread-pool) 14 | + [Stop token](#stop-token) 15 | + [Run loop](#run-loop) 16 | + [Cancellation](#cancellation) 17 | + [Signal handling](#signal-handling) 18 | + [And more!](#and-more) 19 | 20 | ### sender/receiver 21 | 22 | ```cpp 23 | // An echo server example. 24 | 25 | using uring_exec::io_uring_exec; 26 | 27 | // READ -> WRITE -> [CLOSE] 28 | // <- 29 | stdexec::sender 30 | auto echo(io_uring_exec::scheduler scheduler, int client_fd) { 31 | return 32 | stdexec::just(std::array{}) 33 | | stdexec::let_value([=](auto &buf) { 34 | return 35 | uring_exec::async_read(scheduler, client_fd, buf.data(), buf.size()) 36 | | stdexec::then([=, &buf](int read_bytes) { 37 | auto copy = std::ranges::copy; 38 | auto view = buf | std::views::take(read_bytes); 39 | auto to_console = std::ostream_iterator{std::cout}; 40 | copy(view, to_console); 41 | return read_bytes; 42 | }) 43 | | stdexec::let_value([=, &buf](int read_bytes) { 44 | return uring_exec::async_write(scheduler, client_fd, buf.data(), read_bytes); 45 | }) 46 | | stdexec::let_value([=, &buf](int written_bytes) { 47 | return stdexec::just(written_bytes == 0 || buf[0] == '@'); 48 | }) 49 | | exec::repeat_effect_until(); 50 | }) 51 | | stdexec::let_value([=] { 52 | std::cout << "Closing client..." << std::endl; 53 | return uring_exec::async_close(scheduler, client_fd) | stdexec::then([](...){}); 54 | }); 55 | } 56 | 57 | // ACCEPT -> ACCEPT 58 | // -> ECHO 59 | stdexec::sender 60 | auto server(io_uring_exec::scheduler scheduler, int server_fd, exec::async_scope &scope) { 61 | return 62 | uring_exec::async_accept(scheduler, server_fd, nullptr, nullptr, 0) 63 | | stdexec::let_value([=, &scope](int client_fd) { 64 | scope.spawn(echo(scheduler, client_fd)); 65 | return stdexec::just(false); 66 | }) 67 | | exec::repeat_effect_until(); 68 | } 69 | 70 | int main() { 71 | auto server_fd = uring_exec::utils::make_server({.port=8848}); 72 | auto server_fd_cleanup = uring_exec::utils::defer([=] { close(server_fd); }); 73 | 74 | io_uring_exec uring({.uring_entries=512}); 75 | exec::async_scope scope; 76 | 77 | stdexec::scheduler auto scheduler = uring.get_scheduler(); 78 | 79 | scope.spawn( 80 | stdexec::schedule(scheduler) 81 | | stdexec::let_value([=, &scope] { 82 | return server(scheduler, server_fd, scope); 83 | }) 84 | ); 85 | 86 | // Run infinitely. 87 | uring.run(); 88 | } 89 | ``` 90 | 91 | ### C++20 coroutine 92 | 93 | ```cpp 94 | // C++20 coroutine styles. 95 | using uring_exec::io_uring_exec; 96 | 97 | int main() { 98 | io_uring_exec uring(512); 99 | stdexec::scheduler auto scheduler = uring.get_scheduler(); 100 | 101 | std::jthread j {[&](std::stop_token stop_token) { 102 | // Run until a stop request is received. 103 | uring.run(stop_token); 104 | }}; 105 | 106 | auto [n] = stdexec::sync_wait(std::invoke( 107 | [=](auto scheduler) -> exec::task { 108 | co_await exec::reschedule_coroutine_on(scheduler); 109 | // Scheduled to the specified execution context. 110 | 111 | println("hello stdexec! and ..."); 112 | co_await uring_exec::async_wait(scheduler, 2s); 113 | 114 | std::string_view hi = "hello coroutine!\n"; 115 | stdexec::sender auto s = 116 | uring_exec::async_write(scheduler, STDOUT_FILENO, hi.data(), hi.size()); 117 | co_return co_await std::move(s); 118 | }, scheduler) 119 | ).value(); 120 | 121 | // Automatically rescheduled to the main thread. 122 | 123 | println("written bytes: ", n); 124 | } 125 | ``` 126 | 127 | ### Thread pool 128 | 129 | ```cpp 130 | // `uring_exec::io_uring_exec` is MT-safe. 131 | using uring_exec::io_uring_exec; 132 | 133 | int main() { 134 | io_uring_exec uring({.uring_entries=512}); 135 | stdexec::scheduler auto scheduler = uring.get_scheduler(); 136 | exec::async_scope scope; 137 | 138 | constexpr size_t pool_size = 4; 139 | constexpr size_t user_number = 4; 140 | constexpr size_t some = 10000; 141 | 142 | std::atomic refcnt {}; 143 | 144 | auto thread_pool = std::array{}; 145 | for(auto &&j : thread_pool) { 146 | j = std::jthread([&](auto token) { uring.run(token); }); 147 | } 148 | 149 | auto users = std::array{}; 150 | auto user_request = [&refcnt](int i) { 151 | refcnt.fetch_add(i, std::memory_order::relaxed); 152 | }; 153 | auto user_frequency = std::views::iota(1) | std::views::take(some); 154 | auto user_post_requests = [&] { 155 | for(auto i : user_frequency) { 156 | stdexec::sender auto s = 157 | stdexec::schedule(scheduler) 158 | | stdexec::then([&, i] { user_request(i); }); 159 | scope.spawn(std::move(s)); 160 | } 161 | }; 162 | 163 | for(auto &&j : users) j = std::jthread(user_post_requests); 164 | for(auto &&j : users) j.join(); 165 | // Fire but don't forget. 166 | stdexec::sync_wait(scope.on_empty()); 167 | 168 | assert(refcnt == [&](...) { 169 | size_t sum = 0; 170 | for(auto i : user_frequency) sum += i; 171 | return sum * user_number; 172 | } ("Check refcnt value.")); 173 | 174 | std::cout << "done: " << refcnt << std::endl; 175 | } 176 | ``` 177 | 178 | ### Stop token 179 | 180 | ```cpp 181 | using uring_exec::io_uring_exec; 182 | 183 | int main() { 184 | // Default behavior: Infinite run(). 185 | // { 186 | // io_uring_exec uring({.uring_entries=8}); 187 | // std::jthread j([&] { uring.run(); }); 188 | // } 189 | 190 | // Per-run() user-defined stop source (external stop token). 191 | auto user_defined = [](auto stop_source) { 192 | io_uring_exec uring({.uring_entries=8}); 193 | auto stoppable_run = [&](auto stop_token) { uring.run(stop_token); }; 194 | std::jthread j(stoppable_run, stop_source.get_token()); 195 | stop_source.request_stop(); 196 | }; 197 | user_defined(std::stop_source {}); 198 | user_defined(stdexec::inplace_stop_source {}); 199 | std::cout << "case 1: stopped." << std::endl; 200 | 201 | // Per-io_uring_exec stop source. 202 | { 203 | using uring_stop_source_type = io_uring_exec::underlying_stop_source_type; 204 | static_assert( 205 | std::is_same_v || 206 | std::is_same_v 207 | ); 208 | io_uring_exec uring({.uring_entries=8}); 209 | std::jthread j([&] { uring.run(); }); 210 | uring.request_stop(); 211 | } 212 | std::cout << "case 2: stopped." << std::endl; 213 | 214 | // Per-std::jthread stop source. 215 | { 216 | io_uring_exec uring({.uring_entries=8}); 217 | std::jthread j([&](std::stop_token token) { uring.run(token); }); 218 | } 219 | std::cout << "case 3: stopped." << std::endl; 220 | 221 | // Heuristic algorithm (autoquit). 222 | { 223 | io_uring_exec uring({.uring_entries=8}); 224 | constexpr auto autoquit_policy = io_uring_exec::run_policy {.autoquit=true}; 225 | std::jthread j([&] { uring.run(); }); 226 | } 227 | std::cout << "case 4: stopped." << std::endl; 228 | } 229 | ``` 230 | 231 | ### Run loop 232 | 233 | ```cpp 234 | /// Policies. 235 | struct run_policy { 236 | // Informal forward progress guarantees. 237 | // NOTES: 238 | // * These are exclusive flags, but using bool (not enum) for simplification. 239 | // * `weakly_concurrent` is not a C++ standard part, which can make progress eventually 240 | // with lower overhead compared to `concurrent`, provided it is used properly. 241 | // * `parallel` (which makes progress per `step`) is NOT supported for IO operations. 242 | bool concurrent {true}; // which requires that a thread makes progress eventually. 243 | bool weakly_parallel {false}; // which does not require that the thread makes progress. 244 | bool weakly_concurrent {false}; // which requires that a thread may make progress eventually. 245 | 246 | // Event handling. 247 | // Any combination is welcome. 248 | bool launch {true}; 249 | bool submit {true}; 250 | bool iodone {true}; 251 | 252 | // Behavior details. 253 | bool busyloop {false}; // No yield. 254 | bool autoquit {false}; // `concurrent` runs infinitely by default. 255 | bool waitable {false}; // Submit and wait. 256 | bool hookable {true}; // Always true beacause of per-object vtable. 257 | bool detached {false}; // Ignore stop requests from `io_uring_exec`. 258 | bool progress {false}; // run() returns run_progress_info. 259 | bool no_delay {false}; // Complete I/O as fast as possible. 260 | bool blocking {false}; // in-flight operations cannot be interrupted by a stop request. 261 | 262 | bool transfer {false}; // For stopeed local context. Just a tricky restart. 263 | bool terminal {false}; // For stopped remote context. Cancel All. 264 | 265 | size_t iodone_batch {64}; // (Roughly estimated value) for `io_uring_peek_batch_cqe`. 266 | size_t iodone_maxnr {512}; // Maximum number of `cqe`s that can be taken in one step. 267 | }; 268 | 269 | /// Global interface. 270 | // run_policy: See the comments above. 271 | // any_stop_token_t: Compatible with `std::jthread` and `std::stop_token` for a unified interface. 272 | // Return type: Either `run_progress_info` or `void`, depending on `run_policy.progress`. 273 | template 275 | auto run(any_stop_token_t external_stop_token = {}); 276 | 277 | /// Example. 278 | int main() { 279 | uring_exec::io_uring_exec uring({.uring_entries=8}); 280 | // You can also use C++20 designated initializer. 281 | constexpr auto policy = [] { 282 | auto policy = run_policy{}; 283 | policy.concurrent = false; 284 | policy.weakly_parallel = true; 285 | policy.iodone = false; 286 | policy.blocking = true; 287 | return policy; 288 | } (); 289 | std::jthread j([&](auto token) { 290 | uring.run(token); 291 | }); 292 | } 293 | ``` 294 | 295 | ### Cancellation 296 | 297 | ```cpp 298 | int main() { 299 | io_uring_exec uring({.uring_entries = 8}); 300 | std::array threads; 301 | for(auto &&j : threads) { 302 | j = std::jthread([&](auto token) { uring.run(token); }); 303 | } 304 | using namespace std::chrono_literals; 305 | stdexec::scheduler auto s = uring.get_scheduler(); 306 | stdexec::sender auto _3s = make_sender(s, 3s); 307 | stdexec::sender auto _9s = make_sender(s, 9s); 308 | // Waiting for 3 seconds, not 9 seconds. 309 | stdexec::sender auto any = exec::when_any(std::move(_3s), std::move(_9s)); 310 | stdexec::sync_wait(std::move(any)); 311 | } 312 | ``` 313 | 314 | ### Signal handling 315 | 316 | ```cpp 317 | // Modified from `examples/timer.cpp`. 318 | int main() { 319 | io_uring_exec uring(512); 320 | stdexec::scheduler auto scheduler = uring.get_scheduler(); 321 | 322 | std::cout << "start." << std::endl; 323 | 324 | auto s1 = 325 | stdexec::schedule(scheduler) 326 | | stdexec::let_value([=](auto &&...) { 327 | return uring_exec::async_wait(scheduler, 2s); 328 | }) 329 | | stdexec::let_value([=](auto &&...) { 330 | std::cout << "s1:2s" << std::endl; 331 | return stdexec::just(); 332 | }) 333 | | stdexec::let_value([=](auto &&...) { 334 | return uring_exec::async_wait(scheduler, 2s); 335 | }) 336 | | stdexec::let_value([=](auto &&...) { 337 | std::cout << "s1:4s" << std::endl; 338 | return stdexec::just(); 339 | }); 340 | 341 | auto s2 = 342 | stdexec::schedule(scheduler) 343 | | stdexec::let_value([=](auto &&...) { 344 | return uring_exec::async_wait(scheduler, 1s); 345 | }) 346 | | stdexec::let_value([=](auto &&...){ 347 | std::cout << "s2:1s" << std::endl; 348 | return stdexec::just(); 349 | }) 350 | | stdexec::let_value([=](auto &&...) { 351 | return uring_exec::async_wait(scheduler, 2s); 352 | }) 353 | | stdexec::let_value([=](auto &&...) { 354 | std::cout << "s2:3s" << std::endl; 355 | return stdexec::just(); 356 | }); 357 | 358 | // Async_sigwait for specified signals. 359 | stdexec::sender auto signal_watchdog = 360 | uring_exec::async_sigwait(scheduler, std::array {SIGINT, SIGUSR1}); 361 | 362 | // Block all/some signals in ctor. 363 | // Restore previous signals in dtor (or .reset()). 364 | // Examples: 365 | // // Block all signals. 366 | // auto sb = signal_blocker(); 367 | // 368 | // // Block a single SIGINT signal. 369 | // auto sb = signal_blocker(SIGINT); 370 | // 371 | // // Block SIGINT and SIGUSR1 singls. 372 | // // (std::vector or other ranges are also acceptable.) 373 | // auto sb = signal_blocker(std::array {SIGINT, SIGUSR1}); 374 | // 375 | // // Block all signals except SIGINT and SIGUSR1. 376 | // auto sb = signal_blocker(std::array {SIGINT, SIGUSR1}); 377 | auto sb = uring_exec::signal_blocker(); 378 | 379 | std::jthread j([&](auto token) { uring.run(token); }); 380 | // $ kill -USR1 $(pgrep signal_handling) 381 | stdexec::sync_wait( 382 | exec::when_any( 383 | stdexec::when_all(std::move(s1), std::move(s2)) 384 | | stdexec::then([](auto &&...) { std::cout << "timer!" << std::endl; }), 385 | stdexec::starts_on(scheduler, std::move(signal_watchdog)) 386 | | stdexec::then([](auto &&...) { std::cout << "signal!" << std::endl; }) 387 | ) 388 | ); 389 | std::cout << "bye." << std::endl; 390 | } 391 | ``` 392 | 393 | ### And more! 394 | 395 | See the [`/examples`](/examples) directory for more usage examples. 396 | 397 | ## Build 398 | 399 | This is a C++20 header-only library; simply include it in your project. 400 | 401 | If you want to try some examples or benchmark tests, use `xmake`: 402 | * `xmake build examples`: Build all example files. 403 | * `xmake run `: Run a specified example application. (For example, `xmake run hello_coro`.) 404 | * `xmake build benchmarks && xmake run benchmarks`: Build and run the ping-pong test. 405 | 406 | `make` is also supported, but you should ensure that: 407 | * Both `stdexec` and `liburing` are available locally. 408 | * `asio` is optional. 409 | 410 | Then you can: 411 | * `make all`: Build all examples and benchmarks. 412 | * `make `: Build a specified example file. 413 | * `make `: Build a specified benchmark file. 414 | * `make benchmark_script`: Run the ping-pong test. 415 | 416 | It is recommended to use at least Linux kernel version 6.1. 417 | 418 | ## Benchmark 419 | 420 | Here is my benchmark report on: 421 | * {Linux v6.4.8} 422 | * {AMD 5800H, 16 GB} 423 | * {uring_exec 22a6674, asio 62481a2} 424 | * {gcc v13.2.0 -O3} 425 | * {ping-pong: blocksize = 16384, timeout = 5s, throughput unit = GiB/s} 426 | 427 | | threads / sessions | asio (io_uring) | uring_exec | 428 | | ------------------ | --------------- | ---------- | 429 | | 2 / 10 | 1.868 | 3.409 | 430 | | 2 / 100 | 2.744 | 3.870 | 431 | | 2 / 1000 | 1.382 | 2.270 | 432 | | 4 / 10 | 1.771 | 3.164 | 433 | | 4 / 100 | 2.694 | 3.477 | 434 | | 4 / 1000 | 1.275 | 4.411 | 435 | | 8 / 10 | 0.978 | 2.522 | 436 | | 8 / 100 | 2.107 | 2.676 | 437 | | 8 / 1000 | 1.177 | 3.956 | 438 | 439 | See the [`/benchmarks`](/benchmarks) directory for more details. 440 | 441 | ## Core APIs (senders) 442 | 443 | ```cpp 444 | // io_uring_prep_*(sqe, ...) -> async_*(scheduler, ...) 445 | inline constexpr auto async_oepnat = make_uring_sender_v; 446 | inline constexpr auto async_readv = make_uring_sender_v; 447 | inline constexpr auto async_readv2 = make_uring_sender_v; 448 | inline constexpr auto async_writev = make_uring_sender_v; 449 | inline constexpr auto async_writev2 = make_uring_sender_v; 450 | inline constexpr auto async_close = make_uring_sender_v; 451 | inline constexpr auto async_socket = make_uring_sender_v; 452 | inline constexpr auto async_bind = make_uring_sender_v; 453 | inline constexpr auto async_accept = make_uring_sender_v; 454 | inline constexpr auto async_connect = make_uring_sender_v; 455 | inline constexpr auto async_send = make_uring_sender_v; 456 | inline constexpr auto async_recv = make_uring_sender_v; 457 | inline constexpr auto async_sendmsg = make_uring_sender_v; 458 | inline constexpr auto async_recvmsg = make_uring_sender_v; 459 | inline constexpr auto async_shutdown = make_uring_sender_v; 460 | inline constexpr auto async_poll_add = make_uring_sender_v; 461 | inline constexpr auto async_poll_update = make_uring_sender_v; 462 | inline constexpr auto async_poll_remove = make_uring_sender_v; 463 | inline constexpr auto async_timeout = make_uring_sender_v; 464 | inline constexpr auto async_futex_wait = make_uring_sender_v; 465 | inline constexpr auto async_futex_wake = make_uring_sender_v; 466 | inline constexpr auto async_nop = /* (scheduler) */; 467 | inline constexpr auto async_read = /* (scheduler, int fd, void *buf, size_t n) */; 468 | inline constexpr auto async_write = /* (scheduler, int fd, const void *buf, size_t n) */; 469 | inline constexpr auto async_wait = /* (scheduler, steady_clock::duration duration) */; 470 | inline constexpr auto async_sigwait = /* (scheduler, std::ranges::range auto signals) */; 471 | ``` 472 | 473 | See the [`io_uring_exec_sender.h`](/include/uring_exec/io_uring_exec_sender.h) file for more details. 474 | 475 | ## Notes 476 | 477 | + This project was originally a subproject of [io_uring-examples-cpp](https://github.com/Caturra000/io_uring-examples-cpp). 478 | + Although `stdexec` provides official io_uring examples, it does not support any I/O operations. 479 | --------------------------------------------------------------------------------