As an aside I believe that all kind of blocking in a destructor (and thus jthread) is generally a mistake. In order of preference I believe that:
- enforce explicit join, abort or detach via the type system (hard to do in C++)
- abort the async computation
- abort the whole program (i.e. assert)
- detach the async computation
are all preferable to blocking. I think I'm in the minority though.
I'm a big believer in crash-only software and I think that "clanup" at shutdown is code smell and a symptom of unreliable software [1]. Just call _Exit when the application is done.
[1] Cleaning up to decrease the noise for the benefit of a leak detector is useful though, but by experience, I'm not yet convinced that the effort is worth it.
Destruction, in general, is one of the most complex areas of software development in my opinion. You're right, blocking or throwing errors during destruction is terrible, but you really do have to genuinely clean things up when exiting, you can't leave messes behind like this. However, in addition to that: you can't ever count on destruction occuring, because it's entirely possible that your program crashes, the user kills the process, the computer runs out of battery (or someone janks the cable out) or some other random thing causes your process to exit prematurely.
You really need a lot of nuance in this area, and I don't agree with "cleanup at shutdown is a code smell". But you're certainly right that you can go too far in the other direction as well: destruction has to be fast, non-blocking, error-free, and never required.
Well yes, that's why you can't wait for shutdown time to cleanup your mess. In this case, either the files need to survive process exit or they can be unlinked as soon as they are created. Another option is to have very simple supervisor process whose only job is to cleanup.
For a DB like sqlite it does makes sense to have some amount of shutdown cleanup to close transactions and mark the log as stale and avoid any recovery on startup, but it should be considered an optimization and not be a requirement.
To make this short, I do in fact agree with your nuanced view. Pragmatism always trumps dogmatism in software engineering.
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).
int main(int argc, char[][]argv) {
std::jthread t([](){
volatile int a;
while(true) a=1; });
}
will block forever, than it is for this: int main(int argc, char[][]argv) {
std::jthread t([](){
volatile int a;
while(true) a=1; });
t.join();
}
//using the volatiles since while(true); is UB, nothing to do with threading
This is even more true for a class that has a std::jthread member.I've also asserted that the api of std::jthread::join is not great, that it would have been pereferable to have a mandatory timeout parameter, so that someone calling it would be forced to think about an appropriate timeout, since blocking forever is rarely the right choice (of course, you would have a constant for blocking forever as well - this is not about preventing this behavior, just slightly discouraging it).
If you agree that blocking forever waiting for a thread to join is in itself something to be discouraged (not made impossible, but also not the default), then it also follows that having a destructor block at all should also be avoided in the std lib.
I do want to emphasize that I'm not claiming that there are no cases where this sort of behavior is useful, just that it is more rarely the best choice. I also believe in the idea that language/library design should nudge users into the right direction, so that the better use case is also the easier one to write.
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.
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.
This rule of thumb is often useful but there are many exceptions.
As an example, the internal behaviour of this OpenCL function [0] is presumably quite different depending on which bits of the second parameter are set, but it's still a good design as all possible behaviours of the function are still closely related as far as the user is concerned.
[0] https://www.khronos.org/registry/OpenCL/sdk/1.2/docs/man/xht...
And yes there are exceptions, that's why my comment had so many rhetorical devices to defuse those silly arguments. My comment is specifically about "booleans and two behaviors" and even then I don't affirm it as the only valid way, only as my opinion and not in all cases.
The point I've been making is: std::thread is an absolutely terrible candidate for something that you can expect to only have "two behaviors" moving forward. This isn't even a prediction; you just have to look at current platform APIs to see this. I'd cite Windows's CREATE_SUSPENDED as one example of another flag you might want, but someone would inevitably complain and tell me that's just the Windows API being unnecessarily overcomplicated. So, instead, go ahead and check out the sheer behemoth that is POSIX's "simple" interface: the pthread_attr API used to specify different behaviors for pthread_create. It's not just a flag, it's not just a struct... it's a list of structs where each one contains flags as well as other things. And Linux's clone() is the other extreme, where they pack a gazillion flags (read: behaviors) into one integer parameter for thread creation where you'd think it'd leave a little more room for extensibility. Thread creation isn't just something that might need "more than two behavior", it's pretty much the diametric opposite of "two behaviors" if I've ever seen one!
Between your answer and MaxBarraclough's, one is tonally a quickwitted mean quip and the other is a well articulated and well meaning response.
They may still inherit from the same implementation and do what you suggest though.
Why is this useful in this case, is my question. You can try to put lots of things into the type system that aren't, but that doesn't automatically make them better. In fact I'd argue it makes things worse here, e.g., now every container will be instantiated twice depending on whether you use jthreads or threads, almost doubling the amount of work the compiler has to do for them.
As for instantiations, an astute library developer will be able to do the right thing. It's not that hard.
Re container, they will only be instantiated twice if you actually use both std::jthread and std::thread of course. The idea (which I disagree) is that jthread is the better solution and std::thread is sort of deprecated.
This seems a little backwards to me? If the thread should join on destruction then that flag should be set. If not then it should be clear. I'd argue that flag should probably be set in general (and it seems you might feel the same way), but if you have a reason to clear it, then you're just fighting your own program by ignoring it.
> Re container, they will only be instantiated twice if you actually use both std::jthread and std::thread of course. The idea (which I disagree) is that jthread is the better solution and std::thread is sort of deprecated.
Good point!
Except that's not really the case: std::thread is joinable as well. The difference between the two is what happens in the dtor, respectively "terminate" and "join".
I personally don't know how it helps. But I'm sure the proposals had some reasoning.
I really don't think it does personally. You can argue one way or the other with respect to safety (and I would very much agree that terminating on drop is insane), but that's really your backstop, and it's not really relevant to the person who is given a thread: they should know what they want to do with it, and if they want to join it they ought do so.
What is the usage of a "non-joinable" thread in the first place? I mentioned in a comment that that concept itself seems like a code smell to me, and certainly not something that should be the default. See here to read why: https://news.ycombinator.com/item?id=26142754
The one difference between std::thread and jthread is that std::thread terminates on destruction, while jthread joins. That could easily be a ctor parameter (as well as "detach on destruction" as a third option).
The ability to pause a thread from outside is interesting, but it doesn’t seem very useful in general.
It also comes with risks. What if it’s holding a lock? That could cause a performance/latency disaster.
There are coroutines though, that can fill some of the same needs covered by lightweight threads: https://en.cppreference.com/w/cpp/language/coroutines
>>> The ability to pause a thread from outside is interesting, but it doesn’t seem very useful in general.
>>> It also comes with risks. What if it’s holding a lock? That could cause a performance/latency disaster.
Which you replied to, not claiming that it couldn't be paused from outside, but instead that the lock risk was just the same as returning from a function.
Yes, you can try compiling everything static and then distributing the fat binary. It might work on most systems. But if it's dynamically linked then it'll fail with libc++ errors locally (local libc++ not having c++$latest support).