3 ms·
I think there are two schools of thought on this issue: 1. Blocking/unblocking is a major side effect, so it should be handled automatically so you don't forge
by simiones 6y ago
I think there are two schools of thought on this issue:
1. Blocking/unblocking is a major side effect, so it should be handled automatically so you don't forget about it
2. Blocking/unblocking is a major side effect, so it should be very prominent in the code so you don't miss it when reading, and it doesn't accidentally happen when you don't expect it
The C++ designers usually favor the first school, leading to designs like `std::lock_guard`. Many others, myself included, favor the second school, and would complain that the code marker that a method will block forever waiting for a thread to finish shouldn't be "}".
I would also say that in general blocking forever should not be the default - you should have to explicitly ask for it, not have it as a default. This could be achieved with a RAII design by passing the timeout parameter to the constructor, but that obscures the meaning of the } even more, especially if the thread ownership ever changes hands (a function receiving a std::unique_ptr<std::jthread> would wait an unknown amount of time on a }, for example).
- gpderetta 6y agoSo, I guess I fall in the middle. I have no issues with lock_guard. Also, it will unlock on destruction, which is not much of a problem. And having this sort of side effect at end of scope is consistent with languages having synchronized or atomic blocks. I would also not have much of a problem with a join_guard that joins one or more thread on destruction. I am a fan of RAII and both lock_guard and join_guard have a single job and if you use them it is pretty obvious what you want, you can opt in on them and you can delegate the job by passing them around. My issue is with more complex objects blocking on destruction when it is not their primary purpose.
- dataflow 6y agoThe thing is, you talk about how blocking implicitly is bad, but I didn't say "it's good to block in the destructor" as a blanket rule either. I'm talking about this very particular case of blocking deserves to be in the destructor, for what I thought were fairly decent reasons that I spelled out. You haven't actually addressed my points about threads specifically and are just talking about blocking in a destructor in general, but the discussion/disagreement isn't about other cases. There are definitely times when I think it'd be wrong to block implicitly, but this just isn't one of them.
- gpderetta 6y agoNot the parent, but mine is not a theoretical argument. I work on an highly threaded application. Many components spawn threads, send messages to other components running on other threads and wait for messages or events from other components. When you have base classes and members all blocking in their destructors is very easy to end up with deadlocks or race conditions just by changing the order of members or base classes and things like that. By having an explicit (asynchronous) shutdown phase this can be avoided. There have been some talk in the committee about having async destructors. That might be the solution to the problem.
- dataflow 6y agoSo 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).