Replies: 7 comments 4 replies
-
|
Hey @eao197, thanks for sharing! That's an interesting topic. I would be interested in the (experimental) code, if you have some that can be disclosed. I think the main point worth highlighting is the purpose of coroutines in agents (I know you were a bit reluctant at the beginning of the discussion): they (can) enable better concurrency without breaking the actor model. Coroutines give dispatchers more flexibility in thread usage. For example, if two agents share a However, for agents bound to dedicated threads, the benefit is less evident, but coroutines can still simplify asynchronous workflows and integrate with modern async APIs. What do you think? |
Beta Was this translation helpful? Give feedback.
-
Maybe this case has a natural and simple solution: when an agent uses own custom event queue, then it pushes a special execution_demand into the dispatcher queue. This execution_demand has to be treated as not-thread-safe one, because it will modify internal agent's state (extraction of the actual pending message from the custom queue). It means that agents with custom event queues simply can't have thread-safe event handlers. "Can't" means that an agent can make a subscription with the |
Beta Was this translation helpful? Give feedback.
-
|
If a coroutine-like event-handler like this: so_5::resumable_t evt_sample(const msg_sample & msg) {
...
co_await some_action(msg.field);
...
}will be transformed to something like that: void _initial_point(const msg_sample & msg) {
// We need a subscription for the hidden signal.
so_subscribe_self().event(&my_agent::_completion_point);
// We need to pause normal delivery of pending demands
// to this agent.
_so_deactivate_normal_processing();
// Get a resumable object for the coroutine.
_m_current_resumable = this->evt_sample(msg);
// Just return the control back to the dispatcher.
}Then it's possible to get a dangling reference to It means that the lifetime of an incident has to be prolonged until completion of the coroutine. So the message_ref to the incident has to be stored along with a resumable object. Maybe something like that: namespace so_5
{
struct current_coro_info_t {
message_ref_t m_incident;
resumable_t m_resumable;
...
};
} /* namespace so_5 */
class my_agent : public so_5::agent_t {
// Information about the current active coroutine.
so_5::current_coro_info_t _m_current_coro_info;
...
void _initial_point(const so_5::message_ref_t & incident) {
// We need a subscription for the hidden signal.
so_subscribe_self().event(&my_agent::_completion_point);
// We need to pause normal delivery of pending demands
// to this agent.
_so_deactivate_normal_processing();
// Get a resumable object for the coroutine.
_m_current_coro_info = so_5::current_coro_info_t{
.m_incident = incident,
.m_resumable = this->evt_sample(extract_reference_from(incident))
};
// Just return the control back to the dispatcher.
}
void _completion_point(mhood_t<_msg_resume_current_coro>) {
_m_current_coro_info.m_resumable.resume();
if(_m_current_coro_info.m_resumable.done()) {
_m_current_coro_info.destroy();
so_drop_subscription(
so_direct_mbox(),
so_current_state(),
&my_agent::_completion_point);
_so_reactivate_normal_processing();
}
}
...
} Because |
Beta Was this translation helpful? Give feedback.
-
|
One of the initial white spots was delivery of If I understand C++20 coroutines correctly, it's a task of the awaiter object to wake up a coroutine that sleeps on A::task_type
some_class::initiate_connect()
{
auto client_socket = co_await A::connect_to(remote_ip, connect_timeout);
... // result processing.
}The initiate_connect will be a coroutine. But it uses an awaiter object that is provided by library A. And the coroutine will be awakened somewhere from A internals by the following mechanism:
This rather simple picture becomes more complex when we're speaking about event-handlers of SObjectizer's agents. Those event-handlers must be resumed on the appropriate worker thread. The A library doesn't know about SObjectizer's threads. In the best case the A will just call But how to do that if There is an idea how it could be done. DISCLAIMER: I don't know yet if it is implementable. Just write it here as a memo because I don't know when I will get time to try to make an experiment. The first ingredient. A helper coroutine has to be created in the agent's constructor: enum class awakener_action_t { resume_next, quit };
so_5::coro::awakener_task_t
awakener()
{
bool need_quit = false;
while( !need_quit )
{
const auto action = co_await query_next_action();
switch( action )
{
case awakener_action_t::resume_next:
... // sending next msg_resume_current_coroutine.
break;
case awakener_action_t::quit:
need_quit = true;
break;
}
}
}The result The next ingredient is a special wrapper for an arbitrary Awaiter: template< typename Awaiter >
class wrapped_awaiter_t
{
so_5::coro::awakener_task_t m_awakener;
Awaiter m_original;
public:
template< typename SomeAwaiterType >
wrapped_awaiter_t(
so_5::coro::awakener_task_t awakener,
SomeAwaiterType && original )
: m_awakener{ std::move(awakener) }
, m_original{ std::forward<SomeAwaiterType>(original) }
{}
[[nodiscard]]
bool
await_ready() noexcept(noexcept(m_original.await_ready()))
{
return m_original.await_ready();
}
auto
await_suspend(std::coroutine_handle<so_5::coro::promise_type_t>)
{
return m_original.await_suspend(m_awakener);
}
auto
await_ready()
{
return m_original.await_ready();
}
};The trick is to replace an instance of the The next ingredient is the // Helper to get Awaiter instance from Awaitable expression.
// Borrowed from:
// https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await
template<typename Awaitable>
decltype(auto) get_awaiter(Awaitable&& awaitable)
{
if constexpr (has_member_operator_co_await_v<Awaitable>)
return static_cast<Awaitable&&>(awaitable).operator co_await();
else if constexpr (has_non_member_operator_co_await_v<Awaitable&&>)
return operator co_await(static_cast<Awaitable&&>(awaitable));
else
return static_cast<Awaitable&&>(awaitable);
}
class promise_type_t
{
so_5::coro::awakener_task_t m_awakener;
public:
...
template< typename Awaitable >
auto
await_transform( Awaitable && awaitable )
{
using awaiter_t = std::decay_t< decltype(
get_awaiter(std::forward<Awaitable>(awaitable))) >;
return wrapped_awaiter_t< awaiter_t >{
m_awakener,
get_awaiter( std::forward<Awaitable>(awaitable) )
};
}
};With such ingredients we can achieve the desired behaviour. For example: so_5::resumable_t
some_agent::evt_connect(mhood_t<msg_connect> cmd)
{
...
auto client_socket = co_await A::connect(cmd->remote_ip, connect_timeout);
... // handling of the result.
}In that case a call to
|
Beta Was this translation helpful? Give feedback.
-
|
Yet another aspect: it seems that the so_5::resumable_t
my_agent::evt_connect(mhood_t<msg_connect> cmd)
{
...
auto socket = co_await A::connect_to(cmd->remote_ip, connection_timeout); // (1)
if(socket)
{
so_change_state(st_connected);
co_await connection_listener.register_new_connect(socket); // (2)
...
}
}Here we have to receive two
But the second |
Beta Was this translation helpful? Give feedback.
-
|
It seems that the delivery of But there is a possibility of losing It means that when an execution_demand is being passed to Even if this exception is caught, the problem remains: the suspended agent won't be resumed because Because of that I think that sending of I also think that this fact should be expressed in the code in the following way: The virtual void
push_evt_resume_current_coroutine(
execution_demand_t demand ) noexcept;In SObjectizer-5.8 this method will have the default implementation: void
event_queue_t::push_evt_resume_current_coroutine(
execution_demand_t demand) noexcept
{
this->push( demand ); // Just delegate the work.
}This default implementation allows compatibility with existing custom event_queue implementations. The downside -- it will crash the whole application if But in SObjectizer-5.9 this method will become a pure virtual method. So the custom implementations of the The |
Beta Was this translation helpful? Give feedback.
-
|
The proposed approach is not compatible with prio_one_thread dispatcher. The main idea of prio_one_thread dispatcher is to allow processing of low priority agents' demands only after handling demands of the higher priority agents. Let's suppose that we have an agent Let's suppose that the event-handler for so_5::resumable_t
coordinator::evt_reconfigure(mhood_t<reconfigure> cmd) {
...
auto params = co_await params_extractor.extract_new_params();
...
}It means that actual processing of the
But the dispatcher doesn't know about the relation between It would lead to the case when the |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
These are my first thoughts about supporting C++20 coroutines inside an agent's event_handler.
I'm not going to try to fantasize about the purposes that coroutines in event_handler can be used for. It doesn't matter for now. Just assume that we want to have a possibility to write something like that:
The first key moment is a special return value type --
so_5::resumable_t(the name can be changed if something more appropriate will be found).The next key moment is a special handling of event_handlers that return
so_5::resumable_t. When an event_handler usesso_5::resumable_tas the return type then SO-5 will understand that event_handler is a coroutine and will do some special transformation. Let's suppose that we have something like that:This code will be "transformed" into something like this:
The next key moment is the presence of two special methods
_so_deactivate_normal_processingand_so_reactivate_normal_processing.In the normal processing mode there is an event_queue with pending demands (at the moment such a queue belongs to dispatchers, agents do not own these queues).
A demand in the queue holds information about one incoming message and how it should be processed (as normal message or as evt_start/evt_finish special signals). A dispatcher takes the first pending demand, calls an event_handler and then goes to the next pending demand if any.
It could be a case where the demand queue for
my_agentholds several pending demands:The dispatcher gets the first one (for
some_msg) and calls the event_handler for it. It will be the special_evt_auto_generated_for_some_msghandler that just stores the resumable object into_m_current_resumableand returns.The dispatcher sees that event_handled completed and takes the next demand from the queue.
But this normal processing is not appropriate when the event_handler for
some_msgis a coroutine. In this case the normal processing has to be paused and the agent should react to_msg_resume_current_coroutinesignals only.The new hypothetical methods
_so_deactivate_normal_processingand_so_reactivate_normal_processingare intended for switching from normal processing mode to the special one, and for returning back to the normal mode.When the agent switches to this special mode it will look like switching to a different and empty demand queue, the old demand queue will be stored somewhere. This new queue will be used for
_msg_resume_current_coroutinesignals only. All other messages will be redirected to the old queue (they will be stored to the old queue, but the processing will be delayed).When
_msg_resume_current_coroutinearrives it will be extracted and delivered to_evt_auto_generated_for_some_msghandler the usual way. The SO-5 will see such a signal just as usual without a need for a special handling.And the next key moment is to use
_msg_resume_current_coroutinesignal for resumption of a suspended coroutine. The promise and awaitables for SO-5 have to be written in such a way to send_msg_resume_current_coroutinewhen coroutine has to be resumed.This is a very basic idea with a lot of white spots.
The main of them is how to implement this changing of demand queues for an agent. It seems to me that the work on this old idea has to be resumed. If an agent has its custom event_queue then it could be changed to any other queue when it's appropriate and necessary.
Another white spot is support coroutine-based thread safe event handlers on adv_thread_pool dispatcher. The example shown above requires changing of the agent internal members for processing coroutine, but those changes are prohibited in thread safe event handlers. I hope the solution will be found. Even if this won't happen in the near future the first implementation for support of coroutines could be made for non-thread safe event handlers. With the hope that the solution will be found with time.
I think that one way to add support for C++20 coroutines into SO-5 is to use a special mark that enables coroutine-based event_handlers for an agent. Something like that:
An attempt to subscribe an event_handler that returns
so_5::resumable_twithout addition ofso_5::enable_coroutinesto the agent context will lead to an exception during subscription.I also think that SO-5.8 will still require C++17 as the minimum, the support for coroutines will be available only if SO-5 is being compiled in C++20/23 mode.
Beta Was this translation helpful? Give feedback.
All reactions