Stiffstream / sobjectizer

An implementation of Actor, Publish-Subscribe, and CSP models in one rather small C++ framework. With performance, quality, and stability proved by years in the production.

Home Page:https://stiffstream.com/en/products/sobjectizer.html

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Synchronous shutdown

gabm opened this issue · comments

commented

I experiment with switching to sobjectizer in an existing project. By now, the objects in this project get created and destroyed dynamically and they have internal threading (event loops) that get shutdown and waited for in the destructor.

I removed those internal threads and replaced them by companion actors that do the event handling. That works, but I want to make sure that an actor has actually shut down, before finishing the destructor. From what I see, this is currently not happening in a thread safe manor...

Example code

class MyClassCompanion : public so_5::agent_t {
    MyClass& _parent;
    // to things with parent on events
};


class MyClass {
    MyClassCompanion* _actor;
    MyClass() {
                so_5::introduce_child_coop(env.get_root_coop(), [&](so_5::coop_t& coop) {
                    _actor = coop.make_agent<MyClassCompanion>(*this);
                });
    }

    ~MyClass() {
          _actor->so_deregister_agent_coop_normally();
    }
};

When I run valgrind on my solution I see, that sometimes the actor is still accessing its _parent, although the destructor had finished. That leads me to the conclusion that so_deregister_agent_coop_normally is not synchronous, that after it returns something is still running...

  1. is so_deregister_agent_coop_normally thread-safe and synchronous?
  2. is the way of spawning agents in the constructor correct? is that thread-safe?

Hi!

Agents should be dynamically created objects. I suppose you have something like that:

class MyClass {
    MyClassCompanion * _actor; // pointer, not value. 

Am I right?

commented

yes of course, that was a mistake in my sample code... fixed that.

considering that the constructor and destructor calls of the MyClass instance are coming from another thread than the dispatchter that runs the agent... I am concerned about the thread safety

Ok, let me try to explain.

There is the usual C++ object lifetime: at some point the constructor is called and an object is created, then at some point the destructor is called and the object is gone.

SObjectizer makes things a bit complex. Agents are created during filling a coop. This is the moment when an object is born. make_agent (or make_agent_with_binder) returns you a pointer to a new agent. But you can use that pointer safely only while coop is not registered (in your example it's the moment until the return from introduce_child_coop).

Once an agent is registered the interaction with the agent should only be performed via message-passing. You can't hold a pointer to an agent because this pointer can become invalid at any point.

SObjectizer guarantees that the agent will live until its cooperation is registered.

At some point of time you initiate deregistration of the agent's coop (by calling so_deregister_agent_coop_normally for example). This action isn't synchronous. It just marks the coop as deregistering, but the deregistration procedure can take some time. It means that the agent can continue its work after the return from so_deregister_agent_coop_normally.

When deregistration procedure completes the SObjectizer destroys agents from the deregistered coop. The destructor for the agent will be called at that moment. Please note that agent isn't part of SObjectizer Environment anymore when the destructor is called.

As far as I understand you have to have two agents: the parent one (instance of MyClass in your example) and the child (instance of MyClassCompanion). And you want to destroy the child when your parent "dies".

In SObjectizer it's done automatically by parent-child relationship between cooperations. You don't have to hold a pointer to the child in the parent. Just create a child coop by introduce_child_coop and call so_deregister_agent_coop_normally for your MyClass agent when you need to stop. SObjectizer will deregister the child coop automatically.

So your example could look like:

class MyClassCompanion : public so_5::agent_t {
  MyClass & _parent; // It's safe to hold that reference, because the child will be deleted before the parent.
  ...
};

class MyClass : public so_5::agent_t {
  // No need to hold pointer to MyClassCompanion.

  void so_evt_start() override {
    // A child coop can be created only when the parent is started inside SObjectizer.
    so_5::introduce_child_coop(*this, [&](so_5::coop_t & coop) {
        coop.make_agent<MyClassCompanion>(*this);
      });
  }

But if you can't make an instance of MyClass as an agent then there has to be a different story...

@gabm, is this info enough for you? If not, I can provide more explanations.

commented

I was just going to answer.. thank you so much for your time and in-depth explanation.

From what I gather so far:

  • constructing and deconstructing agents is done only at certain points (coop creation, sometime after "deregister" is called)
  • saving the pointer to the agent is not safe, saving a reference to the mbox is.

But if you can't make an instance of MyClass as an agent then there has to be a different story...

Unfortunately this is exactly what I am trying to do, because I cant switch the whole project to sobjectizer at the moment. This is why I am taking this rather strange approach. Is there a way to

  • introduce new coops/agents from external threads
  • wait for the full completion of an agent from an external thread

Can you suggest an approach in my case?

Can you suggest an approach in my case?

There is no problem with introduction of new coops from different threads. Registration of a new coop is a thread safe operation.

Awaiting of a completion can be done several ways.

One of them is the use of mchains and coop-dereg-notifications (see example here). Something like:

// Somewhere in your thread.
auto dereg_ch = so_5::create_mchain(env);
...
// Creation of a new coop.
env.introduce_coop([&](so_5::coop_t & coop) {
  coop.add_dereg_notificator(so_5::make_coop_dereg_notificator(dereg_ch->as_mbox()));
  coop.make_agent<MyClassCompanion>(...);
});
...
// When you have to wait the completion.
so_5::receive(from(dereg_ch).handle_all(),
  [](const so_5::msg_coop_deregistered & cmd) {...});

The standard dereg_notificator sends a msg_coop_deregistered message to the specified destination when coop destroyed by SObjectizer.

Note that dereg_notification is just an std::function so you can write your own.

Another approach is to use some classic notification mechanism, like std::future/std::promise from C++ stdlib. Something like:

class MyClassCompanion : public so_5::agent_t {
  std::promise<void> _destroyed;
  ...
public:
  ~MyClassCompanion() override {
    _destroyed.set_value(); // Notify about end of life.
  }

  [[nodiscard]] auto
  get_destroyed_future() { return _destroyed.get_future(); }
  ...
};

// Somewhere in your thread:
std::future<void> destroyed_future;
env.introduce_coop([&](so_5::coop_t & coop) {
  destroyed_future = coop.make_agent<MyClassCompanion>(...)->get_destroyed_future();
});
...
// When you have to wait the completion.
destroyed_future.get();

But this is just a sketch. Maybe it's necessary to wrap std::promise in a shared_ptr to avoid broken_promise exception when MyClassCompanion::_destroyed_future dies.

commented

Okay, that is very helpful. The 2nd approach feels more natural to me, but the 1st might be more idiomatic ;) ... in any case, that will work indeed.

I was afraid that in case I could not introduce new coops from other threads I would need an "agent creation service" of some sort that creates agents on request from inside and so on...that would have complicated life but your information and solutions will work 👍. Thank you very much for your time!


Edit: your documentation is great, but a hint to thread safety for these kind of action might be useful

I think 2nd approach can be made non-intrusive, because any functional object can be set as dereg-notificator. So it's possible to do something like that:

auto destroyed_promise = std::make_shared< std::promise<void> >();
env.introduce_coop([&](so_5::coop_t & coop) {
  coop.add_dereg_notificator(
    [destroyed_promise](so_5::environment_t &, const so_5::coop_handle_t &, const so_5::coop_dereg_reason_t &) {
      destroyed_promise->set_value();
    }
  );
  coop.make_agent<MyClassCompanion>(...);
});
...
// When you have to wait the completion.
destroyed_promise->get_future().get();

In that case you need no to modify your MyClassCompanion agent and add something to it.

your documentation is great, but a hint to thread safety for these kind of action might be useful

Thanks for your feedback, @gabm !
Good documentation can't be written without a questions or complains from users.
I have added a new section into Basic tutorial and a new article to InDepth series. Hope they will be useful.

commented

Wow.. that fast fast and is immensely useful.. thank you!