3 ms·
So let me see if I understand this in a simplified scenario. You're saying your code might be something like: class X { void inform_shutdown(); void run(); }
by dataflow 6y ago
So let me see if I understand this in a simplified scenario. You're saying your code might be something like:
class X { void inform_shutdown(); void run(); };
class A { std::jthread t; };
class B { X x; ~B() { x.inform_shutdown(); } };
class C { A a; B b; };
and initially all is fine, because A's destructor only runs after B is destroyed, and thus messages are exchanged before ~A blocks on the thread, but then the code eventually involves to something like:
class C { B b; A a; };
and suddenly now ~B can't send shutdown messages and hence the thread won't know to exit in the first place. Is this an example of what you're saying?
To me, it sounds like the underlying issue is something else here. It's hard to say without a concrete example, but if the above is accurate, it might be that a hidden dependency that needs to be made explicit. If X::run is running (say) an infinite work loop on its own thread, it is coupled to that thread in some fashion. For example, it might have some kind of message queue to pull from, and that queue belongs to the thread (i.e. you would put the message queue in A). That means X needs a dependency on A just as much as C does, meaning you'd want to hold std::shared_ptr<A> instead of A, and hence you won't have this blocking issue anymore because it will only occur when the last user of A is destroyed.
Obviously your scenario might have some differences from this, but it seems quite plausible to me that the problem might have a different solution than non-joining threads.
- gpderetta 6y agoYes, there are complex dataflow (ah!) dependencies between heterogeneous threads, often bidirectional. Using shared pointers would means trading potential deadlocks with potential leaks due to circular dependencies (I'm not saying it is worse, but it is a tradeoff).
- dataflow 6y agoI'd argue there's no tradeoff there. Cyclic references are also avoidable (and should be avoided) in general in my experience. How to achieve that again requires a specific example, as there's no one-size-fits-all-solution. It's great that we got here actually, because it demonstrates how higher level constraints that seem so rigid and impenetrable actually stem from simpler design considerations like these that are often disregarded in the beginning.
- gpderetta 6y agoOh, we solve it all right. We have single blocking point for business level code [1] at the top level of the program. Everything else is asynchronous and it is done by future combining. [1] We also block on each thread executor event loop (at least those that are not spinning) of course, but that's hidden from the business level logic.