cppreference есть этот пример для co_await, который возобновляет сопрограмму в другом потоке.
Но безопасно ли это делать без синхронизации?
В частности, resuming_on_new_thread() начинается с одного потока, затем возобновляется после co_await с другим потоком, обращающимся к тому же «фрейму» сопрограммы. Обычно, если вы получаете доступ к одним и тем же данным из двух потоков, вам нужна синхронизация.
#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>
auto switch_to_new_thread(std::jthread& out)
{
struct awaitable
{
std::jthread* p_out;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h)
{
std::jthread& out = *p_out;
if (out.joinable())
throw std::runtime_error("Output jthread parameter not empty");
out = std::jthread([h] { h.resume(); });
// Potential undefined behavior: accessing potentially destroyed *this
// std::cout << "New thread ID: " << p_out->get_id() << '\n';
std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK
}
void await_resume() {}
};
return awaitable{&out};
}
struct task
{
struct promise_type
{
task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
task resuming_on_new_thread(std::jthread& out)
{
std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
co_await switch_to_new_thread(out);
// awaiter destroyed here
std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
}
int main()
{
std::jthread out;
resuming_on_new_thread(out);
}
Рассмотрим, есть ли у функции данные:
task resuming_on_new_thread(std::jthread& out)
{
int data = 0; // Store some data using thread A.
std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
co_await switch_to_new_thread(out);
// awaiter destroyed here
std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
std::cout << data << '\n'; // Access data with thread B. **Is this safe?**
}
Да, это безопасно. Конструктор std::jthread
— это синхронизация.
Имеет смысл. Конструктор jthread вызывается в строке out = std::jthread([h] { h.resume(); });
и отвечает за синхронизацию.
Это безопасно. Помните, что когда сопрограмма приостанавливается, она возвращает управление вызывающей стороне. Таким образом, только поток, который возобновляет сопрограмму, может продолжить выполнение тела сопрограммы. В вашем примере сопрограмма запускается в основном потоке, затем приостанавливается, затем возобновляется в out
.
Гонка данных произойдет только в том случае, если вы попытаетесь возобновить одну и ту же сопрограмму из двух разных потоков, что является неопределенным поведением, как указано в примечаниях std::coroutine_handle<Promise>::resume()
здесь
Поведение не определено, если
*this
не относится к приостановленной сопрограмме [...]. Одновременное возобновление сопрограммы может привести к гонке данных.
Подумайте о вопросе, который вы задаете. Скажем, как узнать, что сопрограмма закончила работу и возобновить ее в другом потоке? Если вы это знаете, значит, вы уже выполнили синхронизацию — неявно через какие-то другие классы. Правильный вопрос: «Как гарантировать, что разные потоки не будут одновременно обращаться к сопрограмме, вызывая гонку данных?»