diff --git a/README.md b/README.md index 29cbee8..cadc11c 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ 12. [lifetime extension](lifetime/lifetime_extension.md) 13. [C++20 direct initialization и ссылочные поля](lifetime/direct_initialization_references.md) 14. [Тернарный оператор](lifetime/ternary_operator.md) + 15. [Корутины и время жизнь](lifetime/coroutines_and_lifetimes.md) 5. (Не)работающий синтаксис 1. [Most Vexing Parse](syntax/most_vexing_parse.md) 2. [Const](syntax/const_launder.md) diff --git a/lifetime/coroutines_and_lifetimes.md b/lifetime/coroutines_and_lifetimes.md new file mode 100644 index 0000000..ad48b81 --- /dev/null +++ b/lifetime/coroutines_and_lifetimes.md @@ -0,0 +1,673 @@ +# Корутины: время жизни и смерти + +`async/await` синтаксис плотно вошел в жизнь современных разработчиков: от фронтенда до бэкенда и низкоуровневой системщины. В 2007 году он появился в `F#` и за следующие годы разбежался по множеству языков: C# (2012), Python (2015), JavaScript (2017), Kotlin (2018), Rust (2019), Zig (2020, но в 2024 [убрали](https://github.com/ziglang/zig/wiki/FAQ#what-is-the-status-of-async-in-zig) из-за проблем реализации в self-hosted компиляторе). + +Несмотря на все его неоднозначности ([знаменитая проблема "цветных" функций](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/)), возможность писать простой линейный код вместо классического callback-спагетти для сложных асинхронных задач — полезна и сокращает усилия на прототипирование. + +Чтобы не отставать, С++20 также добавил долгожданную поддержку: вместо `async` функций у нас относительно явные и более общие типы - корутины. И для них тоже есть `await`... простите, `co_await`! A также `co_return` и `co_yield` — в С++ одним махом решилт как проблемы асинхронных-функций, так функций-генераторов... Или создали проблемы с ними... + +К сожалению, поддержка есть, а вот корутин в cтандартной библиотеке нет! Если хотите, реализуйте свои. Но я пожалуй, возьму корутины из boost::asio, чтобы продемонстрировать следующий восхитительный пример. + +```C++ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace boost::asio; +namespace this_coro = boost::asio::this_coro; + +using namespace std::literals::string_literals; +using namespace std::chrono; + +using Request = std::string; + +struct MetricEmitter { + std::string metric_class; + void emit(std::chrono::milliseconds elapsed) const { + std::cout << metric_class << " " << elapsed << "\n"; + } +}; + +// Демонстрационная корутина для иммитации ввода-выводы +awaitable some_io(int delay) { + steady_timer timer(co_await this_coro::executor); + timer.expires_after(milliseconds(delay)); + co_await timer.async_wait(use_awaitable); + co_return; +} + + +awaitable handle_request(const Request& r) { + co_await some_io(15); + std::cout << "Hello " << r << "\n"; + co_return; +} + +template +awaitable process_requests_batch(Requests&& reqs) +requires std::convertible_to, Request> { + auto executor = co_await this_coro::executor; + // добавляем к обработке запроса метрики времени выполнения + auto handle_with_metrics = [metrics = MetricEmitter { "batch_processor"} ](auto&& request) -> awaitable { + auto start = steady_clock::now(); + co_await handle_request(std::move(request)); + auto finish = steady_clock::now(); + metrics.emit(duration_cast(finish - start)); + }; + for (auto&& r: std::move(reqs)) { + // запускаем конкурентное исполнение для каждого реквеста. + co_spawn(executor, handle_with_metrics(std::move(r)), detached); + } + co_return; +} + +awaitable> accept_requests_batch() { + co_return std::vector{ "Adam"s, "Helen"s, "Bob"s }; +} + +awaitable run() { + co_await process_requests_batch(co_await accept_requests_batch()); + co_await some_io(100); +} + +int main() +{ + // Запускаем наши корутины в однопоточном контексте исполнения + boost::asio::io_context io_context(1); + co_spawn(io_context, run(), detached); + io_context.run(); +} +``` + +Вы могли бы предположить, что этот код успешно напечатает три раза +``` +Hello <имя> +batch_processor <время обработки> +``` +в каком-то порядке. + +Но на самом деле он с очень большой вероятностью упадет с ошибкой сегментации. + +Посмотрим, какое приветственное сообщение [покажет](https://godbolt.org/z/KrY5avbqe) нам address sanitizer + +`gcc -std=c++23 -O0 -fsanitize=address` + + +``` +AddressSanitizer:DEADLYSIGNAL +================================================================= +==1==ERROR: AddressSanitizer: SEGV on unknown address 0x00000000001b (pc 0x7a9f129aedf4 bp 0x7a9f12a1b780 sp 0x7fff9f00a228 T0) +==1==The signal is caused by a READ memory access. +==1==Hint: address points to the zero page. + #0 0x7a9f129aedf4 (/lib/x86_64-linux-gnu/libc.so.6+0x1aedf4) (BuildId: 490fef8403240c91833978d494d39e537409b92e) + #1 0x7a9f1288b664 in _IO_file_xsputn (/lib/x86_64-linux-gnu/libc.so.6+0x8b664) (BuildId: 490fef8403240c91833978d494d39e537409b92e) + #2 0x7a9f1287ffd6 in fwrite (/lib/x86_64-linux-gnu/libc.so.6+0x7ffd6) (BuildId: 490fef8403240c91833978d494d39e537409b92e) + #3 0x7a9f12e900ab (/opt/compiler-explorer/gcc-14.2.0/lib64/libasan.so.8+0x820ab) (BuildId: e522418529ce977df366519db3d02a8fbdfe4494) + #4 0x7a9f12ce8d1c in std::basic_ostream >& std::__ostream_insert >(std::basic_ostream >&, char const*, long) (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0x14cd1c) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79) + #5 0x407e03 in handle_request /app/example.cpp:39 + #6 0x40f697 in std::__n4861::coroutine_handle::resume() const /opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/coroutine:137 + #7 0x449403 in boost::asio::detail::awaitable_frame_base::resume() /app/boost/include/boost/asio/impl/awaitable.hpp:501 + #8 0x445ba4 in boost::asio::detail::awaitable_thread::pump() /app/boost/include/boost/asio/impl/awaitable.hpp:770 + #9 0x454bc7 in boost::asio::detail::awaitable_handler::operator()(boost::system::error_code const&) /app/boost/include/boost/asio/impl/use_awaitable.hpp:93 + #10 0x4517e6 in boost::asio::detail::binder1, boost::system::error_code>::operator()() /app/boost/include/boost/asio/detail/bind_handler.hpp:115 + #11 0x44f337 in void boost::asio::detail::handler_work, boost::asio::any_io_executor, void>::complete, boost::system::error_code> >(boost::asio::detail::binder1, boost::system::error_code>&, boost::asio::detail::awaitable_handler&) /app/boost/include/boost/asio/detail/handler_work.hpp:433 + #12 0x44ccfe in boost::asio::detail::wait_handler, boost::asio::any_io_executor>::do_complete(void*, boost::asio::detail::scheduler_operation*, boost::system::error_code const&, unsigned long) /app/boost/include/boost/asio/detail/wait_handler.hpp:76 + #13 0x41747c in boost::asio::detail::scheduler_operation::complete(void*, boost::system::error_code const&, unsigned long) /app/boost/include/boost/asio/detail/scheduler_operation.hpp:40 + #14 0x41e925 in boost::asio::detail::scheduler::do_run_one(boost::asio::detail::conditionally_enabled_mutex::scoped_lock&, boost::asio::detail::scheduler_thread_info&, boost::system::error_code const&) /app/boost/include/boost/asio/detail/impl/scheduler.ipp:493 + #15 0x41dcfb in boost::asio::detail::scheduler::run(boost::system::error_code&) /app/boost/include/boost/asio/detail/impl/scheduler.ipp:210 + #16 0x41f27a in boost::asio::io_context::run() /app/boost/include/boost/asio/impl/io_context.ipp:64 + #17 0x4099bf in main /app/example.cpp:75 +``` +Похоже что ссылка: +```C++ +awaitable handle_request(const Request& r) +``` +Немножечко испортилась... Но что же пошло не так?! + +------ + +Корутины — очень сложные объекты, которые обманчиво просты в использовании из-за синтаксического сахара. В этом же весь смысл! Поддержка `async/await` на уровне языка и компиляторов делает простым то, что всегда было делать сложно вручную... Так в происходит в высокоуровневых и безопасных языках с автоматическим управлением памятью: Python, C#, JavaScript, Kotlin. +Но не в C++. И не в Rust. (И не в Zig). + +В примере выше есть как минимуи **три** точки отказа, содержащих ошибки. Можете подумать об этом, пока мы будем разворачивать проблемы корутин С++. + +### Что такое корутина? + +Здесь можно поупражняться в терминологии, ведь определений много разных. Есть довольно абстрактное и высокоуровневое: +*корутина — это функция, которая может приостановить свою работу, и которую можно возобновить позже*. + +Но есть один нюанс, ведущий к частому недопониманию: "магическим" свойством на самом деле обладает **не функция**, а **объект**, который из функции **возвращается**. + +JavaScript разработчики знают (я надеюсь), что + +```JavaScript +async function myFunction() { + return "Hello"; +} + +// то же самое что и + +function myFunction() { + return Promise.resolve("Hello"); +} +``` + +Аналогично в Rust + +```Rust + +async fn my_foo() -> String +{ + "Hello".to_string() +} + +// то же самое что и +fn my_foo() -> impl Future { + // создает анонимный объект Future + async { + "Hello".to_string() + } +} +``` + +В C++ же нет специального синтаксиса для объявления функции. Вместо этого нужно явно указать тип возвращаемого значения (например, `awaitable`). И требования к этому типу столь сложны, что для этого потребуется целый вложенный список! +1. `awaitable` должен удовлетворять концепту `std::coroutine_handle_traits`. То есть у него должен быть ассоциированный тип: `promise = typename awaitable::promise_type` +2. Тип `promise` должен удовлетворять [документации](https://eel.is/c++draft/dcl.fct.def.coroutine#def:coroutine,promise_type) концепта `Promise` столь сложному для описания, что про корутины в C++ приходится писать отдельные книжки. Но если кратко: promise контролирует поведение операций `co_await`, `co_yield` и `co_return`. +3. Инстанциация `handle = std::coroutine_handle` должна быть успешной +4. Должна быть возможность сконструировать `awaitable` c помощью `promise.get_return_object()` + +И вот только тогда внутри тела функции, возвращающей `awaitable`, можно будет (и часто нужно будет) использовать синтаксический сахар `co_await`, `co_yield`, `co_return` +```C++ +awaitable myFunction() { + co_return "Hello"; +} +``` + +Который рассахаривается в нечто подобное (это приблизительный не-код) + +```C++ +awaitable myFunction() { + using Promise = awaitable::promise_type; + using Handle = std::coroutine_handle; + Promise p; + auto state = new ImplicitlyGeneratedStateMachine(p); + // state = _ 0; + // ... + //{ + // switch(state) { + // case_0: { state = _1; p.initial_suspend(); } + // case _1: { p.yield_value("Hello"); } + // } + // } + return p.get_return_object(); +} +``` + +Если же ни одно из `co_*` ключевых слов в теле функции не присутствует, то никаких магических преобразований не происходит! И без злого умысла, кажется, такое придумать было нельзя! Смотрите-ка + +```C++ +awaitable process_request(const std::string& r) { + co_await some_io(1); + std::cout << r; +} + +awaitable send_dummy_request() { + return process_request("hello"); +} + +int main(){ + boost::asio::io_context io_context(1); + co_spawn(io_context, send_dummy_request(), detached); + io_context.run(); +} +``` +Запускаем. Проверяем. Работает? Успешно ничего [не печатает](https://godbolt.org/z/deEss1dxM)... +Странно... Давайте-ка уберем `some_io()`. + +```C++ +awaitable process_request(const std::string& r) { + std::cout << r; +} +``` + +Запускаем. Проверяем. Получаем что? [Правильно](https://godbolt.org/z/oxx5bs3sb) + +```C++ +: In function 'boost::asio::awaitable process_request(const std::string&)': +:19:73: warning: no return statement in function returning non-void [-Wreturn-type] + 19 | awaitable process_request(const std::string& r) { std::cout << r; } + | ^ +ASM generation compiler returned: 0 +: In function 'boost::asio::awaitable process_request(const std::string&)': +:19:73: warning: no return statement in function returning non-void [-Wreturn-type] + 19 | awaitable process_request(const std::string& r) { std::cout << r; } + | ^ +Execution build compiler returned: 0 +Program returned: 132 +Program terminated with signal: SIGILL +``` +Ну разумеется. Ну конечно. Ведь без волшебных ключевых слов магии нет и наша `process_request` функция ничего не возвращает. А это же [неопределенное поведение](../syntax/missing_return.md)! + +Добавим `co_return`. + +```C++ +awaitable process_request(const std::string& r) { + std::cout << r; + co_return; +} +``` +Снова [пусто](https://godbolt.org/z/P7nEWoErv)... + +Подключаем санитайзер! + +``` +================================================================= +==1==ERROR: AddressSanitizer: stack-use-after-return on address 0x758796100150 at pc 0x7587985d01e6 bp 0x7ffda95e0290 sp 0x7ffda95dfa50 +READ of size 5 at 0x758796100150 thread T0 + #0 0x7587985d01e5 (/opt/compiler-explorer/gcc-14.2.0/lib64/libasan.so.8+0x821e5) (BuildId: e522418529ce977df366519db3d02a8fbdfe4494) + #1 0x758798428d1c in std::basic_ostream >& std::__ostream_insert >(std::basic_ostream >&, char const*, long) (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0x14cd1c) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79) + #2 0x405c47 in process_request /app/example.cpp:21 + #3 0x4082f5 in std::__n4861::coroutine_handle::resume() const /opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/coroutine:137 + #4 0x427e8b in boost::asio::detail::awaitable_frame_base::resume() /app/boost/include/boost/asio/impl/awaitable.hpp:501 + #5 0x425e2c in boost::asio::detail::awaitable_thread::pump() /app/boost/include/boost/asio/impl/awaitable.hpp:770 +``` + +Ой, ссылка `const std::string& r` умерла похоже. Какое несчастье. Почему? + +А вот же: +```C++ +awaitable send_dummy_request() { + return process_request("hello"); +} +``` + +Тут то же нет никаких магических `co_*` ключевых слов. А значит мы просто вызываем функцию `process_request`, она возвращает объект-корутину, который... + +Самое время уточнить что происходит с аргументами функций, использующих `co_*`: они **неявно** копируются **внутрь** неявно создаваемого объекта. Значение копируется как значение. Ссылка — как ссылка. + +```C++ +awaitable myFunction(int arg1, const std::string& arg2) { + using Promise = awaitable::promise_type; + using Handle = std::coroutine_handle; + Promise p; + auto state = new ImplicitlyGeneratedStateMachine(p); + // state = _ 0; + // int __arg1 { arg1 }; + // const std::string& __arg2 { arg2 }; + //{ + // switch(state) { + // case_0: { state = _1; p.initial_suspend(); } + // case _1: { p.yield_value("Hello"); } + // } + // } + return p.get_return_object(); +} +``` + +То есть + +```C++ +awaitable process_request(const std::string& r) {...} + +awaitable send_dummy_request() { + // неявно конструируется локальный временный std::string, ссылка на него передается в + // функцию process_request. Где ссылка дальше копируется внутрь стейт-машины + return process_request("hello"); + // возвращаем стейт-машину, а локальный временный объект умирает. + // Use-after-free при попытке получить результат стейт-машины +} +``` + +Нужны волшебные слова... Между тем, вы еще не чувствуете насколько все может стать плохо в контексте шаблонов? Нет? Когда может быть совершенно не ясно, корутину нам передали или нет? Нет? Ну ничего страшного. Возьмите это в качестве упражнения на дом — написать `std::invoke`, поддерживающий корутины. А мы пока продолжим добавлять волшебные слова + +```C++ +awaitable send_dummy_request() { + co_return process_request("hello"); +} +``` + +``` +: In function 'boost::asio::awaitable send_dummy_request()': +:26:5: error: no member named 'return_value' in 'std::__n4861::coroutine_traits >::promise_type' {aka 'boost::asio::detail::awaitable_frame'} + 26 | co_return process_request("hello"); + | ^~~~~~~~~ +Compiler returned: 1 +``` + +Ну естественно. Ведь `process_request` возвращает корутину... +Давайте, шаг за шагом, напишем, что же на самом деле должно происходить. Ведь мы же хотим понять и разобраться... + +```C++ +awaitable send_dummy_request() { + auto task = process_request("hello"); // функция вернула корутину-стейт-машину + auto result = co_await task; // нужно дождаться завершения + co_return result; // и вернуть результат +} +``` +Ой, небольшая заминочка... + +``` +: In function 'boost::asio::awaitable send_dummy_request()': +:27:28: error: use of deleted function 'boost::asio::awaitable::awaitable(const boost::asio::awaitable&) [with T = void; Executor = boost::asio::any_io_executor]' + 27 | auto result = co_await task; + | ^~~~ +In file included from /app/boost/include/boost/asio/co_spawn.hpp:22, + from :8: +/app/boost/include/boost/asio/awaitable.hpp:123:3: note: declared here + 123 | awaitable(const awaitable&) = delete; + | ^~~~~~~~~ +``` +Это особенность boost asio, для большей безопасности он требует применять `co_await` только к rvalue. +Небольшое исправление. +```C++ +auto result = co_await std::move(task); +``` +И мы получаем... + +``` +:28:10: error: deduced type 'void' for 'result' is incomplete + 28 | auto result = co_await std::move(task); + | ^~~~~~ +``` + +Вы все еще не почувствовали, насколько все может стать плохо с шаблонами? Ничего страшного, помним, что у С++ всегда были проблемы с таким замечательным типом `void`. +Просто объединим `co_return` и `co_await` в одну строчку + +```C++ +awaitable send_dummy_request() { + auto task = process_request("hello"); // функция вернула корутину-стейт-машину + // нужно дождаться завершения + co_return co_await std::move(task); // и вернуть результат +} +``` + +Компилируется [и...](https://godbolt.org/z/16sTer15x) +``` +================================================================= +==1==ERROR: AddressSanitizer: stack-use-after-return on address 0x76cc8d801070 at pc 0x76cc8fa541e6 bp 0x7ffed68d7510 sp 0x7ffed68d6cd0 +READ of size 5 at 0x76cc8d801070 thread T0 + #0 0x76cc8fa541e5 (/opt/compiler-explorer/gcc-14.2.0/lib64/libasan.so.8+0x821e5) (BuildId: e522418529ce977df366519db3d02a8fbdfe4494) + #1 0x76cc8f8acd1c in std::basic_ostream >& std::__ostream_insert >(std::basic_ostream >&, char const*, long) (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0x14cd1c) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79) + #2 0x405c47 in process_request /app/example.cpp:20 + #3 0x408cc7 in std::__n4861::coroutine_handle::resume() const /opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/coroutine:137 + #4 0x428cef in boost::asio::detail::awaitable_frame_base::resume() /app/boost/include/boost/asio/impl/awaitable.hpp:501 + #5 0x426e00 in boost::asio::detail::awaitable_thread::pump() /app/boost/include/boost/asio/impl/awaitable.hpp:770 + #6 0x432170 in boost::asio::detail::awaitable_async_op_handler::operator()() /app/boost/include/boost/asio/impl/awaitable.hpp:804 + #7 0x431a95 in boost::asio::detail::binder0 >::operator()() /app/boost/include/boost/asio/detail/bind_handler.hpp:56 + #8 0x431f04 in void boost::asio::detail::executor_function::complete >, std::allocator >(boost::asio::detail::executor_function::impl_base*, bool) /app/boost/include/boost/asio/detail/executor_function.hpp:113 + #9 0x40a5ac in boost::asio::detail::executor_function::operator()() /app/boost/include/boost/asio/detail/executor_function.hpp:61 + #10 0x42a66d in boost::asio::detail::executor_op, boost::asio::detail::scheduler_operation>::do_complete(void*, boost::asio::detail::scheduler_operation*, boost::system::error_code const&, unsigned long) /app/boost/include/boost/asio/detail/executor_op.hpp:70 + #11 0x410754 in boost::asio::detail::scheduler_operation::complete(void*, boost::system::error_code const&, unsigned long) /app/boost/include/boost/asio/detail/scheduler_operation.hpp:40 + #12 0x41728b in boost::asio::detail::scheduler::do_run_one(boost::asio::detail::conditionally_enabled_mutex::scoped_lock&, boost::asio::detail::scheduler_thread_info&, boost::system::error_code const&) /app/boost/include/boost/asio/detail/impl/scheduler.ipp:493 + #13 0x41680d in boost::asio::detail::scheduler::run(boost::system::error_code&) /app/boost/include/boost/asio/detail/impl/scheduler.ipp:210 + #14 0x417be0 in boost::asio::io_context::run() /app/boost/include/boost/asio/impl/io_context.ipp:64 + #15 0x406b67 in main /app/example.cpp:36 +``` + +Та же самая проблема. Строка умерла. По той же самой причине. +А что если мы теперь соединим все в одну строку кода? + +```C++ +awaitable send_dummy_request() { + co_return co_await process_request("hello"); +} +``` +А вот теперь все наконец-то правильно. И [печатает](https://godbolt.org/z/cjG3dTjnr) заветное слово `"hello"`; + +Здорово, неправда ли, как мы прошли путь от неправильного +```C++ +awaitable send_dummy_request() { + return process_request("hello"); +} +``` +к правильному +```C++ +awaitable send_dummy_request() { + co_return co_await process_request("hello"); +} +``` + +Разве могла бы такая красота получиться, если бы C++ решил все-таки требовать маркировку `[co_]async` у объявления функций? Тогда бы у нас было скучно (прям как в Rust): +```C++ +[[co_async]] awaitable send_dummy_request() { + return process_request("hello"); // Compilation error! Type mismatch / co_return should be used +} +``` + +Но это все была синтаксическая забава, в процессе которой мы поймали ошибку: const ссылки, rvalue ссылки и неявное создание временных объектов. Ссылки неявно захватываются стейт-машиной, а неявные временные объекты неявно умирают. +Создавайте временные переменные явно. Контролируйте их время жизни. Избегайте ссылочных параметров у корутин при возможности. Рецепт простой. Вернемся к нашему самому первому примеру и исправим ошибки и потенциальные проблемы со ссылками. + +```C++ +// Принимаем теперь все параметры для корутин по значению. + +awaitable handle_request(Request r) { + co_await some_io(15); + std::cout << "Hello " << r << "\n"; + co_return; +} + +template +awaitable process_requests_batch(Requests reqs) +requires std::convertible_to, Request> { + auto executor = co_await this_coro::executor; + // добавляем к обработке запроса метрики времени выполнения + auto handle_with_metrics = [metrics = MetricEmitter { "batch_processor"} ](auto request) -> awaitable { + auto start = steady_clock::now(); + co_await handle_request(std::move(request)); + auto finish = steady_clock::now(); + metrics.emit(duration_cast(finish - start)); + }; + for (auto&& r: std::move(reqs)) { + // запускаем конкурентное исполнение для каждого реквеста. + co_spawn(executor, handle_with_metrics(std::move(r)), detached); + } + co_return; +} + +awaitable> accept_requests_batch() { + co_return std::vector{ "Adam"s, "Helen"s, "Bob"s }; +} + +awaitable run() { + co_await process_requests_batch(co_await accept_requests_batch()); + co_await some_io(100); +} +``` + +Компилируем. Запускаем. И получаем... Правильно, [новую ошибку сегментации](https://godbolt.org/z/Y3GnYePvf)! + +``` +================================================================= +==1==ERROR: AddressSanitizer: heap-use-after-free on address 0x511000000228 at pc 0x7c812eca71e6 bp 0x7ffded723390 sp 0x7ffded722b50 +READ of size 15 at 0x511000000228 thread T0 + #0 0x7c812eca71e5 (/opt/compiler-explorer/gcc-14.2.0/lib64/libasan.so.8+0x821e5) (BuildId: e522418529ce977df366519db3d02a8fbdfe4494) + #1 0x7c812eaffd1c in std::basic_ostream >& std::__ostream_insert >(std::basic_ostream >&, char const*, long) (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0x14cd1c) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79) + #2 0x41f592 in MetricEmitter::emit(std::chrono::duration >) const /app/example.cpp:24 + #3 0x40ab8e in operator() /app/example.cpp:52 + #4 0x40f771 in std::__n4861::coroutine_handle::resume() const /opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/coroutine:137 + #5 0x4494e7 in boost::asio::detail::awaitable_frame_base::resume() /app/boost/include/boost/asio/impl/awaitable.hpp:501 + #6 0x445c86 in boost::asio::detail::awaitable_thread::pump() /app/boost/include/boost/asio/impl/awaitable.hpp:770 + #7 0x454cab in boost::asio::detail::awaitable_handler::operator()(boost::system::error_code const&) /app/boost/include/boost/asio/impl/use_awaitable.hpp:93 + #8 0x4518ca in boost::asio::detail::binder1, boost::system::error_code>::operator()() /app/boost/include/boost/asio/detail/bind_handler.hpp:115 + #9 0x44f41b in void boost::asio::detail::handler_work, boost::asio::any_io_executor, void>::complete, boost::system::error_code> >(boost::asio::detail::binder1, boost::system::error_code>&, boost::asio::detail::awaitable_handler&) /app/boost/include/boost/asio/detail/handler_work.hpp:433 + #10 0x44cde2 in boost::asio::detail::wait_handler, boost::asio::any_io_executor>::do_complete(void*, boost::asio::detail::scheduler_operation*, boost::system::error_code const&, unsigned long) /app/boost/include/boost/asio/detail/wait_handler.hpp:76 + #11 0x417556 in boost::asio::detail::scheduler_operation::complete(void*, boost::system::error_code const&, unsigned long) /app/boost/include/boost/asio/detail/scheduler_operation.hpp:40 + #12 0x41e9ff in boost::asio::detail::scheduler::do_run_one(boost::asio::detail::conditionally_enabled_mutex::scoped_lock&, boost::asio::detail::scheduler_thread_info&, boost::system::error_code const&) /app/boost/include/boost/asio/detail/impl/scheduler.ipp:493 + #13 0x41ddd5 in boost::asio::detail::scheduler::run(boost::system::error_code&) /app/boost/include/boost/asio/detail/impl/scheduler.ipp:210 + #14 0x41f354 in boost::asio::io_context::run() /app/boost/include/boost/asio/impl/io_context.ipp:64 + #15 0x4099ae in main /app/example.cpp:75 +``` + +Судя по трейсу, теперь у нас умерла другая строка... Та, что была сохранена в `MetricEmitter` + +```C++ +template +awaitable process_requests_batch(Requests reqs) +requires std::convertible_to, Request> { + auto executor = co_await this_coro::executor; + // добавляем к обработке запроса метрики времени выполнения + auto handle_with_metrics = [metrics = MetricEmitter { "batch_processor"} ](auto request) -> awaitable { + auto start = steady_clock::now(); + co_await handle_request(std::move(request)); + auto finish = steady_clock::now(); + metrics.emit(duration_cast(finish - start)); + }; + for (auto&& r: std::move(reqs)) { + // запускаем конкурентное исполнение для каждого реквеста. + co_spawn(executor, handle_with_metrics(std::move(r)), detached); + } + co_return; +} +``` + +Если вы еще не догадались, позвольте напомнить о еще кое-чем неявном... А также о том что корутинами могут быть и методы классов + +```C++ +struct MetricEmitter { + std::string metric_class; + void emit(std::chrono::milliseconds elapsed) const { + std::cout << metric_class << " " << elapsed << "\n"; + } + + awaitable wrap_request(Request r) const { + auto start = steady_clock::now(); + co_await handle_request(std::move(r)); + auto finish = steady_clock::now(); + // Корутина также неявно захватывает указатель this! + emit(duration_cast(finish - start)); + } +}; + +// так что, наверное, очевидно, что +auto task = MetricEmitter{"batch_process"}.wrap_request(request); +co_await task; // MetricEmitter умер. Будет use-after-free +``` + +У нас в примере кое-что похожее, но другое + +```C++ +// handle_with_metric это анонимная структура с определенным operator() +auto handle_with_metrics = [metrics = MetricEmitter { "batch_processor"} ](auto request) -> awaitable { + auto start = steady_clock::now(); + co_await handle_request(std::move(request)); + auto finish = steady_clock::now(); + // корутина неявно захватывает this... A this в этом случае -- указатель на + // лямбда-функцию! + metrics.emit(duration_cast(finish - start)); +}; +// если лямбда-функция умрет раньше чем завершится исполнение корутины, будет use-after-free +``` + +Смотрим как мы ее вызываем +```C++ + for (auto&& r: std::move(reqs)) { + // запускаем конкурентное исполнение для каждого реквеста... + // В ФОНЕ!!! результат вызова handle_with_metrics -- корутина -- сохраняется куда-то + // внутри функции boost::asio::co_spawn + co_spawn(executor, handle_with_metrics(std::move(r)), detached); + // и будет обработана позже + } + co_return; // а вот тут наша лямбда и умрет. +``` + +Под такие неприятные случаи у `co_spawn` есть перегрузка, принимающая напрямую функцию, а не `awaitable`. Но у функции тогда не должно быть аргументов. + +Ошибку можно исправить разными способами, следуя рекомендации: все параметры корутины нужно передать явно и переместить внутрь ее тела. В C++23, для методов классов, с этим может помочь `deduced this` + +```C++ +struct MetricEmitter { + std::string metric_class; + void emit(std::chrono::milliseconds elapsed) const { + std::cout << metric_class << " " << elapsed << "\n"; + } + + awaitable wrap_request(this auto self, Request r) { + // self скопирован по значению! + auto start = steady_clock::now(); + co_await handle_request(std::move(r)); + auto finish = steady_clock::now(); + // Корутина также неявно захватывает указатель this! + self.emit(duration_cast(finish - start)); + } +}; + +``` +А из statefull лямбд возвращать корутины не рекомендуется. Убирайте список захвата! + + +```C++ +template +awaitable process_requests_batch(Requests reqs) +requires std::convertible_to, Request> { + auto executor = co_await this_coro::executor; + // добавляем к обработке запроса метрики времени выполнения + auto handle_with_metrics = [](auto request) -> awaitable { + auto metrics = MetricEmitter { "batch_processor"}; + auto start = steady_clock::now(); + co_await handle_request(std::move(request)); + auto finish = steady_clock::now(); + metrics.emit(duration_cast(finish - start)); + }; + for (auto&& r: std::move(reqs)) { + // запускаем конкурентное исполнение для каждого реквеста. + co_spawn(executor, handle_with_metrics(std::move(r)), detached); + } + co_return; +} +``` + +Вот теперь все [работает](https://godbolt.org/z/oTW466Y6Y). +``` +Hello Adam +batch_processor 15ms +Hello Helen +batch_processor 15ms +Hello Bob +batch_processor 15ms +``` + +------ +Отслеживать время жизни ссылок в асинхронном коде вручную крайне тяжело. Автоматика, как в Rust, делает это намного лучше, но при этом может выдавать совершенно непонятные репорты, в которых можно разобраться только если знаешь, что именно могло пойти не так — за это async в Rust и не любят и критикуют. А в качестве самого простого и продуктивного решения, чтоб убложить borrow checker, выбирается копирование всего подряд (`.clone()`, везде `.clone()`) + +С++ отдает полный контроль вам с невероятной кучей неявных захватов ссылок! Делайте с ними что хотите и как хотите. Скомпилируется без проблем и проверок. Вы можете приложить ментальные усилия, отследить все ссылки и убедиться что объекты не умрут не вовремя. Либо можно отчаяться, прочитать гайдлайны и передавать все и всегда by value. Копируя и перемещая. Никаких ссылок. + +Проблема со ссылками это лишь начало. Все становится намного хуже, если мы еще и подключим многопоточное выполнение корутин. +- Только внимательность, опыт и, может быть, [статический анализатор](https://clang.llvm.org/extra/clang-tidy/checks/cppcoreguidelines/no-suspend-with-lock.html), помогут вам избежать случайной попытки переместить `std::unique_lock` между потоками, если вы его по какой-то причине продержите через `co_await`. +- Разумеется еще больше возможности для race conditions. Особенно если забыть что-то скопировать внутрь тела корутины. + +Ах да, совсем забыл: в зависимости от реализации, корутины могут быть ленивыми или не очень ленивыми (смотри `promise::initial_suspend()`). Boost::asio::awaitable — ленивые. И потому мы сразу получали прекрасные use-after-free. Для корутин, у которых `promise::initial_suspend()` возвращает `suspend_never`, код вида +```C++ +awaitable process_request(const std::string& r) { + std::cout << r; + co_await something(); + /* r не используется */ + co_return; +} +``` +может продолжать успешно работать и долгое время не вызывать проблем + +---- + +Корутины C++ — гибки, мощны и совершенно небезопасны. Надеюсь, вы настроили сборку и тесты с санитайзерами прежде чем решили ими воспользоваться. + +По состоянию на 2024 год статические анализаторы C++ **частично подсвечивают** подобные проблемы. +- Clang-Tidy имеет агрессивную проверку на [использование ссылок в корутинах](https://clang.llvm.org/extra/clang-tidy/checks/cppcoreguidelines/avoid-reference-coroutine-parameters.html) +- Также может быть настроен на подсвечивание [statefull лябмд](https://clang.llvm.org/extra/clang-tidy/checks/cppcoreguidelines/avoid-capturing-lambda-coroutines.html) +- Но они не скажут, что вам нужно использовать `co_return co_await foo()` вместо `return foo()`. + +Но в общем случае это не ошибки. Можно успешно использовать ссылки и не платить за копии. Можно также избегать лишнего обертывания в слой корутины и делать `return foo()` напрямую. + + +## Полезные ссылки + +1. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rcoro-capture +2. https://en.cppreference.com/w/cpp/language/coroutines +3. https://reductor.dev/cpp/2023/08/10/the-downsides-of-coroutines.html \ No newline at end of file diff --git a/numeric/unsigned_unary_minus.md b/numeric/unsigned_unary_minus.md index 5199bda..a971413 100644 --- a/numeric/unsigned_unary_minus.md +++ b/numeric/unsigned_unary_minus.md @@ -1,6 +1,6 @@ # Унарный минус и беззнаковые числа -Вы разрабатываете граффический интерфейс для игры. У вас уже есть кнопочки, панельки, иконки. Все отлично. И вот вы решаете, чтоб интерфейс ощущался интереснее, реализовать анимацию для _check box_ элемента -- при нажатии для снятия галочки, check_box элемент будет красиво отъезжать в сторону где-то на 30% своей ширины. +Вы разрабатываете графический интерфейс для игры. У вас уже есть кнопочки, панельки, иконки. Все отлично. И вот вы решаете, чтоб интерфейс ощущался интереснее, реализовать анимацию для _check box_ элемента -- при нажатии для снятия галочки, check_box элемент будет красиво отъезжать в сторону где-то на 30% своей ширины. У вас были подобные структуры и функции @@ -35,7 +35,7 @@ void on_unchecked(ElementID el) { } ``` -Ваш check_box имел ширину 50 пикселей. Вы запустили тест... И элемент полностью улетел за пределы экрана! +Ваш check_box имел ширину 50 пикселей. Вы запустили тест... И элемент улетел за пределы экрана! Вы пошли смотреть логи и [обнаружили](https://godbolt.org/z/hbccqG5r8) ``` diff --git a/pointer_provenance/misaligned_reference.md b/pointer_provenance/misaligned_reference.md index 431ef9c..01cd3ca 100644 --- a/pointer_provenance/misaligned_reference.md +++ b/pointer_provenance/misaligned_reference.md @@ -71,7 +71,7 @@ std::cout << std::format("{} {} {}", r.data, r.status, r.value); // все тр ``` Да, "универсальная ссылка" это все еще ссылка. -В упакованное структуре поля не выровнены. Ссылки на них брать нельзя. +В упакованной структуре поля не выровнены. Ссылки на них брать нельзя. Но ведь же в первоначальном варианте с одной структурой работало без предупреждений... diff --git a/pointer_provenance/strict_aliasing.md b/pointer_provenance/strict_aliasing.md index fbcf59a..42101e4 100644 --- a/pointer_provenance/strict_aliasing.md +++ b/pointer_provenance/strict_aliasing.md @@ -165,9 +165,9 @@ pub struct Data<'a, T> { #[no_mangle] pub unsafe fn process(data: &mut Data<'_, usize>, idx: usize) { *data.data.get(idx) -= 1; // ссылка &mut usize - // она не может алиасить полу step + // она не может алиасить поле step // так как иначе это бы означало, что data содержит ссылку на саму себя - // что невозможно для обычных ссылок в Rust + // что невозможно для обычных ссылок в safe Rust if (*data.data.get(idx) == 0) { // можно оптимизировать! data.counter -= 1; } @@ -188,7 +188,7 @@ process: Ну хорошо, существует такое правило. Оптимизировать позволяет. Отлично. А где неопределенное поведение? -А оно начинается тогда, когда разработчик начинает вытворять безобразие с преобразованием указателей! +А оно начинается тогда, когда разработчик творит безобразие с преобразованием указателей! Вот теперь можно и классическую странную функцию показать! @@ -240,7 +240,7 @@ Type punning очень распространен в библиотеках с Туpe punning с помощью `reinterpret_cast` — почти всегда выводит на поле неопределенное поведение. Язык C (не C++!) позволяет выполнять type punning с помощью union. -В C++ до C++20 нужно было использовать `memcpy` и... копировать. Компиляторы при этом часто способны копии ради type punning оптимизировать. В C++20 добавили `std::bit_cast` для той же цели. +В C++ до C++20 нужно было использовать `memcpy` и... копировать. Компиляторы при этом часто способны такие копии ради type punning оптимизировать и убирать. В C++20 добавили `std::bit_cast` для той же цели. В С++23 еще появится `start_lifetime_as` — который поможет для наиболее частого случая type punning: интерпретировать массив принятых байт (например, по сети) как осмысленную структуру/массив структур.