Introduction
Since the very beginning there were only immutable messages in SObjectizer-5.
Immutable message is a very simple and safe approach to implement an interaction in a concurrent application:
- a message instance can be received by any number of receivers at the same time;
- a message can be redirected to any number of new receivers;
- a message can be stored to be processed later...
Because of that immutable messages are very useful in 1:N or N:M interactions. And because Publish-Subscribe Model was the first model supported by SObjectizer-5 the interaction via immutable messages is used by default.
But there can be cases when immutable message is not a good choice in 1:1 interaction...
Example 1: Big Messages
Let's consider a pipeline of agents which need to modify a big binary object...
A message like this:
std::array<std::uint8_t, 10*1024*1024> raw_data_;
...
};
A base class for agent messages.
That need to be processed by a pipeline like that:
void on_fragment(mhood_t<raw_image_fragment> cmd) {
...
next_stage_->deliver_message(cmd.make_reference());
}
...
};
void on_fragment(mhood_t<raw_image_fragment> cmd) {
...
next_stage_->deliver_message(cmd.make_reference());
}
...
};
But... It won't be compiled!
void on_fragment(mhood_t<raw_image_fragment> cmd) {
cmd->raw_data_[0] = ...;
...
next_stage_->deliver_message(cmd.make_reference());
}
...
};
void on_fragment(mhood_t<raw_image_fragment> cmd) {
cmd->raw_data_[1] = ...;
...
next_stage_->deliver_message(cmd.make_reference());
}
...
};
A safe way is copy, modify and send modified copy...
void on_fragment(mhood_t<raw_image_fragment> cmd) {
auto cp = std::make_unique<raw_image_fragment>(*cmd);
cp->raw_data_[0] = ...;
...
next_stage_->deliver_message(std::move(cp));
}
...
};
void on_fragment(mhood_t<raw_image_fragment> cmd) {
auto cp = std::make_unique<raw_image_fragment>(*cmd);
cp->raw_data_[1] = ...;
...
next_stage_->deliver_message(std::move(cp));
}
...
};
It's obvious that the safe way is a very, very inefficient...
Example 2: Messages With Moveable Data Inside
Let's consider a case where agent Alice opens a file and then transfers opened file to agent Bob:
std::ifstream file_;
process_file(std::ifstream file) : file_(
std::move(file)) {}
};
...
void on_handle_file(mhood_t<start_file_processing> cmd) {
std::ifstream file(cmd->file_name());
}
};
...
void on_process_file(mhood_t<process_file> cmd) {
...
}
};
void send(Target &&to, Args &&... args)
A utility function for creating and delivering a message or a signal.
But if we try to do something like that:
...
void on_process_file(mhood_t<process_file> cmd) {
std::ifstream file(std::move(cmd->file_));
...
}
};
We will get a compile-time error at point (1) because cmd->file_ is const and can't be moved anywhere...
There Are Some Workarounds Of Course...
You can declare fields of your messages as mutable:
mutable std::array<std::uint8_t, 10*1024*1024> raw_data_;
...
};
void on_fragment(mhood_t<raw_image_fragment> cmd) {
cmd->raw_data_[0] = ...;
...
next_stage_->deliver_message(cmd.make_reference());
}
...
};
But what if your message is received by two agents at the same time? There is no any guarantee that message will be delivered only to the single receiver...
Or you can use shared_ptr instead of object itself:
std::shared_ptr<std::ifstream> file_;
process_file(std::shared_ptr<std::ifstream> file) : file_(
std::move(file)) {}
};
...
void on_handle_file(mhood_t<start_file_processing> cmd) {
auto file = std::make_shared<std::ifstream>(cmd->file_name());
}
};
But there is additional memory allocation and additional level of data indirection. Overhead can be significant if you need to transfer small objects like mutexes.
The Real Solution: Mutable Messages
Since v.5.5.19 a message of type Msg can be sent either as immutable one:
void send_delayed(Target &&target, std::chrono::steady_clock::duration pause, Args &&... args)
A utility function for creating and delivering a delayed message to the specified destination.
and as mutable one:
To receive and handle a mutable message an event handler must have on of the following formats:
A message wrapped to be used as type of argument for event handlers.
A special marker for mutable message.
Note, that mutable_mhood_t<M> is just a shorthand for mhood_t<mutable_msg<M>>. Usage of mutable_mhood_t<M> makes code more compact and concise. But mhood_t<mutable_msg<M>> can be used in templates:
template<typename M>
...
void on_message(mhood_t<M> cmd) {
...
}
};
With mutable messages the examples above can be rewritten that way:
An example with big messages:
std::array<std::uint8_t, 10*1024*1024> raw_data_;
...
};
void on_fragment(mutable_mhood_t<raw_image_fragment> cmd) {
cmd->raw_data_[0] = ...;
...
}
...
};
void on_fragment(mutable_mhood_t<raw_image_fragment> cmd) {
cmd->raw_data_[1] = ...;
...
}
...
};
An example with moveable object inside:
std::ifstream file_;
process_file(std::ifstream file) : file_(
std::move(file)) {}
};
...
void on_handle_file(mhood_t<start_file_processing> cmd) {
std::ifstream file(cmd->file_name());
}
};
...
void on_process_file(mutable_mhood_t<process_file> cmd) {
std::ifstream file(std::move(cmd->file_));
...
}
};
Safety Of Mutable Messages
But why sending of a mutable message is safer that sending an immutable message with mutable fields inside? Are there some guarantees from SObjectizer?
A mutable message can be sent only to MPSC mbox or mchain. It means that there can be at most one receiver of the message. An attempt to send mutable message to MPMC mbox will lead to an exception at run-time.
A mutable_mhood_t<M> works just like std::unique_ptr: when you redirect your mutable message to someone else your mutable_mhood_t becomes nullptr.
It means that you lost your access to mutable message after redirection:
void on_fragment(mutable_mhood_t<raw_image_fragment> cmd) {
cmd->raw_data_[0] = ...;
...
cmd->raw_data_[0] = ...;
}
These all mean that only one receiver can have access to mutable message instance at some time.
This property can't be satisfied for immutable message.
And this makes usage of mutable messages safe.
Immutable And Mutable Message Are Different
Mutable message of type M has different type than immutable message of type M. It means that an agent can have different event handlers for mutable and immutable M:
struct M final {};
public :
two_handlers(context_t ctx) :
so_5::agent_t(
std::move(ctx)) {
.
event(&two_handlers::on_immutable_M)
.event(&two_handlers::on_mutable_M);
}
}
private :
void on_immutable_M(mhood_t<M>) { std::cout << "on immutable" << std::endl; }
};
subscription_bind_t so_subscribe_self()
Initiate subscription to agent's direct mbox.
virtual void so_evt_start()
Hook on agent start inside SObjectizer.
std::enable_if< details::is_agent_method_pointer< details::method_arity::unary, Method_Pointer >::value, subscription_bind_t & >::type event(Method_Pointer pfn, thread_safety_t thread_safety=not_thread_safe)
Make subscription to the message.
Private part of message limit implementation.
Mutable Messages In Synchronous Interaction
A mutable message can be used for service requests (e.g. for synchronous interactions):
public :
service_provider(context_t ctx) :
so_5::agent_t(
std::move(ctx)) {
*cmd = "<" + *cmd + ">";
return std::move(*cmd);
});
}
...
};
...
so_5::mbox_t provider_mbox = ...;
auto r = so_5::request_value<std::string, so_5::mutable_msg<std::string>>(
const infinite_wait_indication infinite_wait
A special indicator for infinite waiting on service request or on receive from mchain.
But note: mutable service request can be sent only into MPSC-mbox or mchain.
Conversion Into An Immutable Message
When a mutable message is received via mutable_mhood_t and then redirected via send or request_value/future then redirected message will also be a mutable message. It means that redirected message can be sent only to one subscriber and can be handled only via mutable_mhood_t.
Sometimes it is necessary to remove mutability of a message and send the message as immutable one. It can be done via to_immutable helper function.
Helper function to_immutable converts its argument from mutable_mhood_t<M> into mhood_t<M> and returns message hood to immutable message. This new message hood can be used as parameter for send, request_value or receive_future. Old mutable message hood becomes a nullptr and can't be used anymore.
void some_agent::on_some_message(mutable_mhood_t<some_message> cmd) {
...
...
}
std::enable_if<!is_signal< M >::value, mhood_t< immutable_msg< M > > >::type to_immutable(mhood_t< mutable_msg< M > > msg)
Transform mutable message instance into immutable.
Note: a mutable message can be converted to immutable message only once. An immutable message can't be converted into mutable one.
Mutable Messages And Timers
Mutable messages can be sent by send_delayed functions:
so_environment(), dest_mbox,
std::chrono::milliseconds(200),
...
);
Mutable messages can't be sent as periodic messages. It means that send_periodic can be used with mutable_msg only if a period parameter is zero:
so_environment(), dest_mbox,
std::chrono::milliseconds(200),
std::chrono::milliseconds::zero(),
...);
so_environment(), dest_mbox,
std::chrono::milliseconds(200),
std::chrono::milliseconds(150),
...);
timer_id_t send_periodic(Target &&target, std::chrono::steady_clock::duration pause, std::chrono::steady_clock::duration period, Args &&... args)
A utility function for creating and delivering a periodic message to the specified destination.
Signals Can't Be Mutable
Signals do not carry any information inside. Because of that there is no sense in mutable_msg<S> where S is a signal type.
Because of that an attempt to use mutable_msg<S> in code will lead to compile-time error if S is a signal.