Why an entirely new class? Why not just pass a constructor argument to specify joining behavior? It's not like there's any reason to believe this is the only thing we'll ever want to customize for a thread.
I don't think making blocking on destruction conditional is a good thing. For example futures returned by std::async block on destruciton as opposed to other futures (even if they have the same type). This is generally considered a mistake as the future behaves very differently depending on how it was created and it can cause issues (i.e. deadlocks) for code that did not expect the blocking behaviour.
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.
Could you elaborate on why blocking in a thread's destructor is bad? You mention it is but don't really give any reason. Every time I use std::thread I find the need to wait on it in the destructor. In fact any non-joining thread is a code smell to me. If you just detach (or abort...) a thread, then there's simply no way to ensure that main() will return after the thread has finished—and you always want to make sure all threads finish their tasks and clean up properly before the process terminates. This requires you to wait on threads during app shutdown, which generally corresponds with destruction, which means you need to join on destruction. I actually don't understand why you would want it another way by default (if at all).
I just think that blocking is an important enough effect that that needs to be accounted for and it should be explicit in code. Deadlocking is the obvious reason, but also the possibly large pauses are problematic.
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.
Just last week I was debugging an issue with a library that used SQLite internally, and I noticed that whenever I shut down, it would leave all these "something.db-shm" and "something.db-wal" files around in addition to the "something.db" file. My hunch was the library wasn't properly deinitializing SQLite, and indeed that was the case, if you call sqlite3_close() properly, these files are deleted (they're essentially journal files for SQLite). The library was essentially doing what you're suggesting, ending without doing the proper cleanup.
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.
> 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.
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.
> In this case, either the files need to survive process exit or they can be unlinked as soon as they are created.
The latter literally isn't possible on Windows. And I'd argue it's a good thing because you shouldn't do that in this case, though either way you don't have that option anyway. The notion of having a supervisory process just to clean up your mess that you had every chance to clean up is just poor design. Supervisory processes are for when you can't do that and there's critical work left over, not for cleaning up random clutter you just didn't bother to try to handle at the correct time. Imagine if every program that used SQLite spawned an extra supervisory process? Now imagine if this was true for every program that needed temporary files as long as it was running? That's obviously just terrible. Your code needs to clean up after itself as much as it can, whether via RAII or otherwise, but you can't just throw out the notion of cleaning things up on shutdown (or any other point) altogether!
you do not have a chance to cleanup if you are application is killed anyway. So either don't make a mess, make sure that you can clean up no matter what or live with it.
> you do not have a chance to cleanup if you are application is killed anyway.
Then you live with it in that case. That doesn't mean you should litter when the application is terminating gracefully. You clean up on shutdown when you are able. I don't see what's contentious about this.
Yeah I'd argue these problems happen because people don't take advantage of RAII (maybe because they don't like it or their language doesn't support it), and then forget/neglect to do the proper cleanups at the correct time. You want to design things so that they encapsulate their correctness by design. For a thread, that means joining it on destruction (because that should happen in general). For a mutex, it means not unlocking it on destruction (because that signals another part of the code is actually incorrect). For lock guard, it means unlocking on destruction. For files, it means closing them on destruction. Etc. You can't just reduce everything to "always" or "never".
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).
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.
The 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.
Not 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.
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.
Yes, 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).
I'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.
Oh, 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.
I think the arguments I presented do hold for thread joining as well as other cases: it's less clear from the code that this:
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.
> it's less clear from the code that this will block forever
Sure! That doesn't imply that's a bad thing. It's not like "clarity" is transitive. Do all your callers know when you're calling lock_guard() for example? No. Should they? No.
> than it is for this:
You actually sacrifice correctness here for "clarity". What happens if you return control flow returns to the caller before join() is called, whether normally (early return, etc.) or via an exception? Suddenly you have a thread that is out of your control, and the caller has absolutely no guarantees about any aspect of it. At best, your program might terminate in the middle of that thread's work, and wreck whatever it was doing (maybe it was working on some file or communicating with another system). At worst, your code was called in a loop and you actually bring down the whole system, e.g. by launching new threads before the old ones have terminated. It's a massive bug to return with rogue threads.
> 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.
What I'm saying is this isn't true. Blocking should be discouraged in most cases, but this case is specifically an exception to that. Just like how "calling 'delete' directly should be discouraged" has obvious exceptions like "except in the destructor of a smart pointer". And just like how "calling mutex::unlock() directly should be discourage" has obvious exceptions like "except in the destructor of a lock guard". Your reasoning simply doesn't follow, and for good reason.
Moreover, I also beg to differ on the notion that "every blocking operation should have a timeout"; that's true in certain scenarios such as when you're waiting for a component in a distributed system (like an event on another process or machine that might go down without guaranteed notification to you), but quite false in general. Just look at lock_guard... it has no timeout, and it's used all over the place. Moreover, any operation you do that involves a heap allocation or deallocation can block indefinitely with no timeout, and it's perfectly fine. And if you use Windows, look for example at how GetMessage() has no timeout, and note that it's used in pretty much every GUI program for messaging within threads, between threads, and between processes. I could cite more examples but hopefully this illustrates what I mean.
The sense I'm getting from your arguments is that you simply don't believe and/or trust in 'scoped' operations. I don't know how to convince you here, but if you scope your operations match destructors to constructors like they're supposed to be, some things that could go wrong simply won't go wrong. Because, more abstractly, this lets you decompose your program into a tree instead of a DAG, and all the niceness that comes with that just comes automatically.
In my opinion, when you have an internal boolean flag for two behaviors, the interface you should probably expose isn't the flag in one function, but two different functions.
Each bit is a Boolean flag (hex, scientific, etc.)... they're just packed into an integer. (Nobody said you have to use N bools for N different flags...)
edit I see I'm late, dataflow already made this point. I'll leave this here anyway.
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.
But you made the point much better than dataflow. Their comment is borderline incendiary.
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.
Come on... I made a point in my first comment, you either miss or ignore it, I cite an example to maybe get it across in case it's more vivid with a obvious example, and instead of at least acknowledging the point and maybe disagreeing with it, you ignore it and say it's "incendiary" and "silly" to post such a vivid example? The counterexample isn't some weird exception; it's quite typical, and especially pertinent for this particular case.
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!
I did not say what you said is "incendiary", I said it is "borderline incendiary". It's not incendiary since it is true, but it is expressed in such a pythy fashion as to be almost incendiary. Especially when I crafted my original comment to specifically acknowledge those cases.
Between your answer and MaxBarraclough's, one is tonally a quickwitted mean quip and the other is a well articulated and well meaning response.
> Please don't post shallow dismissals, especially of other people's work. A good critical comment teaches us something.
That's why I answered to one comment but not to the other. It's really sterile ground for an argument. Which I'd hoped that by carefully wording my first comment we could all avoid.
I'd disagree on that. That would create 2 unrelated types when in fact one of the classes here semantically really is more like a subclass of the other one. Additionally, that creates additional work for the compiler both for this class (makes it hard to put the definition in an implementation file) and for its users (containers will now get instantiated and codegened for each type separately).
> The main reason is so that the type system knows which thread is which and can handle it differently.
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.
It's useful because you may be able to make compile-time decisions on the basis of the type of thread you're dealing with. What those are, I don't know.
As for instantiations, an astute library developer will be able to do the right thing. It's not that hard.
say you have function that returns a thread object (which is perfectly fine as std::thread is moveable). The caller cannot assume that the thread will join on destruction and needs to conservatively join or detach explicitly anyway.
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.
> The caller cannot assume that the thread will join on destruction and needs to conservatively join or detach explicitly anyway.
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.
the function that will be affected by the thread blocking might not be the one that has created the thread so it might not have a chance to set the flag.
Of course you can handle it by declaring and documenting preconditions and so on, but, when possible, enforcing these sort of things at the type level is better.
> The main reason is so that the type system knows which thread is which and can handle it differently.
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 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.
> 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.
You might be onto something with this but it's not obvious to me. I would've thought if the caller says "don't join this on destruction", that means "I know what this thread does better than you because I created it, and you cannot expect to be able to wait for it to exit in the general case" (e.g., maybe it infinite-loops or something). Under what scenario would the callee have a good reason to give you a non-auto-joining thread, and you (the caller) a good reason to override that assessment on destruction by default, and why are these scenarios the desired general behavior? (I can understand special cases, like when you pass a particular message to the thread to get it to quit, but not the general case for destructors.)
std::thread are already joinable (and that's their default state), and jthreads can be detached.
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).
> Joinable thread and non-joinable one are very different in usage.
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
Because std::jthread adds new methods as well. If you don't use a different class, then you end up with a weird situation where you still have to decide what get_stop_source() and get_stop_token() should do when the new constructor argument is omitted. That's definitely less clean, IMO.
I mean, you can just throw an exception or leave it undefined when the state isn't what you expect... isn't that a normal thing with any nontrivial class? e.g., what should unique_lock do if you call lock() when you passed adopt_lock_t to the constructor? What should unique_ptr do if you call operator->() when you passed null to the constructor? What should fstream do if you call open() when you already passed a file name to the constructor? etc.
Any function that returns while holding a lock may be a disaster. That's not specific to threads. This is why you should prefer RAII locked mutexes like std::scoped_lock whenever possible.
What you say is true, but (as far as I can tell) unrelated to the issue at hand. Yes, if you return holding a lock, it's a potential disaster. And if you pause a thread from outside while the thread is holding a lock, it's a similar disaster. But RAII and scoped_lock won't save you if a thread is paused from outside, since the paused thread never makes it out of the scope that took the lock.
Then what was lrossi referring to when he (?) said:
>>> 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.
It's difficult to get the point across on the Internet, but lrossi's statement is just nonsense. std::jthread has not introduced any new risks with respect to locking.
You are right, the sleep functions apply only to “this_thread”. What confused me is the comment for the two functions, which refer to the thread as “t” and not “this_thread”; which also matches the naming used for the “is_joinable” function, where it is not the current thread.
So I thought it’s a way to do back pressure across threads, which could be useful, but I was clearly wrong. My bad, but also not a great choice of naming things in the original article.
Then again, if you can only put the current thread to sleep, why have a function for it? Why not call sleep directly?
Lightweight threads like in Go or Erlang are not happening anytime soon as a C++ standard because they require much more complex runtimes to be decently implemented (whereas traditional threads can just delegate straight to whatever OS threading is available).
Just remember: when you use C++$latest features in your code, you won't be able to distribute, compille and run it on an distro older than a handful of years. That is to say: almost all of them.
Why not? Of course you need an up-to-date compiler, but beyond that I've never heard of any fundamental issues in compiling binaries that work on older systems (well besides installing newer compilers or cross-compiling being a real PITA to set up on Linux sometimes, but that has nothing to do with language features).
Replacing the compiler on a distro is not a simple task and it has system wide consequences that make it very undesirable for continued system stability.
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).
Why can’t you install multiple versions of the compiler on the system? This seems like a poor design choice that could be easily solvable if you move outside the “only run what the distribution blesses” idea.
On linux it should be possible to install multiple versions of libstdc++. But newer versions are binary compatible [1] with older versions so you can just keep the most recent version.
66 comments
[ 3.3 ms ] story [ 133 ms ] threadAs 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.
The latter literally isn't possible on Windows. And I'd argue it's a good thing because you shouldn't do that in this case, though either way you don't have that option anyway. The notion of having a supervisory process just to clean up your mess that you had every chance to clean up is just poor design. Supervisory processes are for when you can't do that and there's critical work left over, not for cleaning up random clutter you just didn't bother to try to handle at the correct time. Imagine if every program that used SQLite spawned an extra supervisory process? Now imagine if this was true for every program that needed temporary files as long as it was running? That's obviously just terrible. Your code needs to clean up after itself as much as it can, whether via RAII or otherwise, but you can't just throw out the notion of cleaning things up on shutdown (or any other point) altogether!
Then you live with it in that case. That doesn't mean you should litter when the application is terminating gracefully. You clean up on shutdown when you are able. I don't see what's contentious about this.
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).
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.
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.
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.
[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.
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.
Sure! That doesn't imply that's a bad thing. It's not like "clarity" is transitive. Do all your callers know when you're calling lock_guard() for example? No. Should they? No.
> than it is for this:
You actually sacrifice correctness here for "clarity". What happens if you return control flow returns to the caller before join() is called, whether normally (early return, etc.) or via an exception? Suddenly you have a thread that is out of your control, and the caller has absolutely no guarantees about any aspect of it. At best, your program might terminate in the middle of that thread's work, and wreck whatever it was doing (maybe it was working on some file or communicating with another system). At worst, your code was called in a loop and you actually bring down the whole system, e.g. by launching new threads before the old ones have terminated. It's a massive bug to return with rogue threads.
> 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.
What I'm saying is this isn't true. Blocking should be discouraged in most cases, but this case is specifically an exception to that. Just like how "calling 'delete' directly should be discouraged" has obvious exceptions like "except in the destructor of a smart pointer". And just like how "calling mutex::unlock() directly should be discourage" has obvious exceptions like "except in the destructor of a lock guard". Your reasoning simply doesn't follow, and for good reason.
Moreover, I also beg to differ on the notion that "every blocking operation should have a timeout"; that's true in certain scenarios such as when you're waiting for a component in a distributed system (like an event on another process or machine that might go down without guaranteed notification to you), but quite false in general. Just look at lock_guard... it has no timeout, and it's used all over the place. Moreover, any operation you do that involves a heap allocation or deallocation can block indefinitely with no timeout, and it's perfectly fine. And if you use Windows, look for example at how GetMessage() has no timeout, and note that it's used in pretty much every GUI program for messaging within threads, between threads, and between processes. I could cite more examples but hopefully this illustrates what I mean.
The sense I'm getting from your arguments is that you simply don't believe and/or trust in 'scoped' operations. I don't know how to convince you here, but if you scope your operations match destructors to constructors like they're supposed to be, some things that could go wrong simply won't go wrong. Because, more abstractly, this lets you decompose your program into a tree instead of a DAG, and all the niceness that comes with that just comes automatically.
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.
> Please respond to the strongest plausible interpretation of what someone says, not a weaker one that's easier to criticize. Assume good faith.
https://news.ycombinator.com/newsguidelines.html
> Please don't post shallow dismissals, especially of other people's work. A good critical comment teaches us something.
That's why I answered to one comment but not to the other. It's really sterile ground for an argument. Which I'd hoped that by carefully wording my first comment we could all avoid.
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!
Of course you can handle it by declaring and documenting preconditions and so on, but, when possible, enforcing these sort of things at the type level is better.
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.
You might be onto something with this but it's not obvious to me. I would've thought if the caller says "don't join this on destruction", that means "I know what this thread does better than you because I created it, and you cannot expect to be able to wait for it to exit in the general case" (e.g., maybe it infinite-loops or something). Under what scenario would the callee have a good reason to give you a non-auto-joining thread, and you (the caller) a good reason to override that assessment on destruction by default, and why are these scenarios the desired general behavior? (I can understand special cases, like when you pass a particular message to the thread to get it to quit, but not the general case for destructors.)
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).
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 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.
>>> 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.
So I thought it’s a way to do back pressure across threads, which could be useful, but I was clearly wrong. My bad, but also not a great choice of naming things in the original article.
Then again, if you can only put the current thread to sleep, why have a function for it? Why not call sleep directly?
There are coroutines though, that can fill some of the same needs covered by lightweight threads: https://en.cppreference.com/w/cpp/language/coroutines
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).
[1] modulo ABI bugs and bug fixes of course.
https://www.youtube.com/watch?v=elFil2VhlH8