Repository navigation
Replies: 2 comments
|
It seems that implicit scheduler with so_5::cpp_coro::task_t<void>
some_task(...)
{
... // Some code with co_await/co_return.
}
int main()
{
auto first_task = some_task(...); // Coroutine created, but not started yet.
auto second_task = some_task(...); // Coroutine created, but not started yet.
...
// Only now we can create a scheduler.
so_5::cpp_coro::this_thread_scheduler_t scheduler{ ... };
so_5::cpp_coro::current_scheduler_setter_t current_scheduler{ scheduler };
...
scheduler.sync_wait(first_task);
...
scheduler.sync_wait(second_task);
...
}In this case an exception will be thrown at the first call of But I don't know if it is a realistic scenario. Even if it is, I don't know how to bind an instance of |
|
Another possible drawback is that it makes it impossible to create a coroutine in one thread and pass it to another thread for execution. For example, with ordinary functions we can do something like that: using my_functor_t = std::function<void()>;
struct msg_transfer_execution : public so_5::message_t
{
my_functor_t m_what;
...
};
void task_maker_agent::evt_some_event(mhood_t<msg_create_task>) {
// Create a function.
my_functor_t f = []() { ... /* some code to be executed somewhere else */ };
// Pass it for execution to another thread.
so_5::send<msg_transfer_execution>(select_free_worker, f);
}
void worker_agent::evt_execute_task(mhood_t<msg_transfer_execution> cmd) {
// Execution of the task created elsewhere.
cmd->m_what();
}But if a coroutine is being bound to a scheduler implicitly during creation then we can't do that: using my_functor_t = std::function<so_5::cpp_coro::task_t<void>()>;
struct msg_transfer_execution : public so_5::message_t
{
my_functor_t m_what;
...
};
void task_maker_agent::evt_some_event(mhood_t<msg_create_task>) {
// Create a coroutine.
my_functor_t f = []() -> so_5::cpp_coro::task_t<void> {
... /* some code to be executed somewhere else */
};
// Pass it for execution to another thread.
so_5::send<msg_transfer_execution>(select_free_worker, f);
}
so_5::cpp_coro::task_t<void>
worker_agent::evt_execute_task(mhood_t<msg_transfer_execution> cmd) {
// Execution of the task created elsewhere.
co_await cmd->m_what();
}And it seems to me that this is a very big drawback. |
Uh oh!
There was an error while loading. Please reload this page.
When implementing support for C++20 coroutines, I proceeded from the fact that the concept of a scheduler is required - an object responsible for resuming the coroutines in the working context desired by a user.
Now this scheduler is explicitly passed, for example, to the asynchronous version of select:
If the asynchronous select is called inside the agent event, then the scheduler for the coroutine can be obtained from the agent itself through the new
agent_t::so_this_agent_schedulermethod:So far, this approach has not caused any problems. However, now in the implementation of support for coroutines in SO-5, I have reached the point where I need to allow using any coroutines inside the event handler, not just those provided by SObjectizer. That is, I need to make sure that the event handler can be written like this:
where
funcfrom the third-party librarythird_partycreates a coroutine that will be executed not in the context of SObjectizer, but in the context owned by the library `third_party'.Accordingly, the event handler
event_with_foreign_corostarts working in the context of SObjectizer, then pauses on co_await, and then resumes, but this time in the context of the library `third_party'. Which is fundamentally wrong, because SObjectizer must ensure that agent handlers work only on those threads to which the agent is bound.To cope with this situation, I planned to implement in
so_5::cpp_coro::task_t::promise_typetheawait_transformmethod, which will cunningly replace theawaitableandawaiterfromthird_partywith specialawaitableandawaiterfrom SObjectizer. This specialawaitablewill be woken up by thethird_partylibrary on someone else's context, butawaitablewill not wake up the source coroutine on this context, but instead will send a special message to SObjectizer so that SObjectizer wakes up the source coroutine on the correct working thread.I already have a prototype of such a solution. But there were several ambiguous points.
First, when
so_5::cpp_coro::task_t::promise_typeis created, the currentschedulermust be saved in thepromiseinstance, which should be used to resume the coroutine. And C++ does not provide an explicit way to do this.For example, if the event handler is a lambda function like:
When the compiler creates the
promiseobject in this lambda function, either thepromise_typeconstructor without parameters will be used, or the constructor with the only parametermhood_t<msg>. There is no way to pass theschedulerexplicitly.Probably the easiest way to achieve the desired result is to implicitly save a pointer to the current
schedulerin a thread-local variable. SObjectizer will do this automatically before calling the agent's event handler.Secondly, initially I thought that
await_transformwould divideawaitableinto ours and aliens. That is, ifawait_transformis called forso_5::cpp_coro::task_t<T>, then nothing is done, the originalawaitableis returned. But ifawait_transformis called forawaitablefrom a third-party library, then the third-partyawaitableis wrapped in our special auxiliaryawaitable.However, it turns out that
so_5::cpp_coro::task_tshould be bound toschedulerimmediately, because this binding is necessary for the proper operation ofawait_transform. And iftask_tis linked to some kind ofscheduler, then where is the guarantee that this is the correctscheduler?In fact, there are no guarantees. And in the current experimental version, you can step on such an unpleasant rake:
There are two agents tied to different work threads.
The agent
a_consumer_tlaunches theselectcoroutine, but binds this coroutine to foreignscheduler'. Therefore, when the coroutine resumes, the agenta_consumer_t` wakes up, but on a different working thread. Which should not be allowed, because then SObjectizer loses one of its main guarantees!Which means that the
await_transformfor the case ofso_5::cpp_coro::task_tcannot have such a trivial implementation:Even for
so_5::cpp_coro::task_t, you will have to return a new specialawaiter. Which is not a good thing in terms of the number of actions performed by the coroutines.Based on the above, I'm starting to think that it might be more correct to have implicit
schedulerobjects.Let's consider working with asynchronous
selectin two situations.Situation one:
selectis used outside of agents. Now it requires writing like this:It could be done like this:
The instance of
current_scheduler_setterwill save a reference to the currentschedulerin the thread-local variable. Andselectwill take a reference from this variable. If there is no link, thenselectwill throw an exception.Situation two:
selectis used inside the agent. Now you have to write like this:It could be done like this:
SObjectizer itself will use
current_scheduler_setter_twhen calling event handlers for agents with coroutines.With this approach, the
await_transformfor the case ofso_5::cpp_coro::task_tcan be made quite simple with a single check inside:If
task_tis linked to anotherscheduler, then an exception will be thrown. But iftask_tis bound to the correctscheduler, then no additional actions are performed.Plus, in many cases, the user will not have to think about the
schedulerat all: in agents, the correctschedulerwill be installed by SObjectizer itself. If the agents are not used, it will be enough to create a "current_scheduler_setter" next to the "this_thread_scheduler".On the other hand, because of the hidden control of the "scheduler" from the user's eyes, there is a feeling of some "magic" - something somehow arises from somewhere. And I personally don't like this kind of implicitness very much.
All reactions