Related
Assume I'm starting a std::thread and then detach() it, so the thread continues executing even though the std::thread that once represented it, goes out of scope.
Assume further that the program does not have a reliable protocol for joining the detached thread1, so the detached thread still runs when main() exits.
I cannot find anything in the standard (more precisely, in the N3797 C++14 draft), which describes what should happen, neither 1.10 nor 30.3 contain pertinent wording.
1 Another, probably equivalent, question is: "can a detached thread ever be joined again", because whatever protocol you're inventing to join, the signalling part would have to be done while the thread was still running, and the OS scheduler might decide to put the thread to sleep for an hour just after signalling was performed with no way for the receiving end to reliably detect that the thread actually finished.
If running out of main() with detached threads running is undefined behaviour, then any use of std::thread::detach() is undefined behaviour unless the main thread never exits2.
Thus, running out of main() with detached threads running must have defined effects. The question is: where (in the C++ standard, not POSIX, not OS docs, ...) are those effects defined.
2 A detached thread cannot be joined (in the sense of std::thread::join()). You can wait for results from detached threads (e.g. via a future from std::packaged_task, or by a counting semaphore or a flag and a condition variable), but that doesn't guarantee that the thread has finished executing. Indeed, unless you put the signalling part into the destructor of the first automatic object of the thread, there will, in general, be code (destructors) that run after the signalling code. If the OS schedules the main thread to consume the result and exit before the detached thread finishes running said destructors, what will^Wis defined to happen?
The answer to the original question "what happens to a detached thread when main() exits" is:
It continues running (because the standard doesn't say it is stopped), and that's well-defined, as long as it touches neither (automatic|thread_local) variables of other threads nor static objects.
This appears to be allowed to allow thread managers as static objects (note in [basic.start.term]/4 says as much, thanks to #dyp for the pointer).
Problems arise when the destruction of static objects has finished, because then execution enters a regime where only code allowed in signal handlers may execute ([basic.start.term]/1, 1st sentence). Of the C++ standard library, that is only the <atomic> library ([support.runtime]/9, 2nd sentence). In particular, that—in general—excludes condition_variable (it's implementation-defined whether that is save to use in a signal handler, because it's not part of <atomic>).
Unless you've unwound your stack at this point, it's hard to see how to avoid undefined behaviour.
The answer to the second question "can detached threads ever be joined again" is:
Yes, with the *_at_thread_exit family of functions (notify_all_at_thread_exit(), std::promise::set_value_at_thread_exit(), ...).
As noted in footnote [2] of the question, signalling a condition variable or a semaphore or an atomic counter is not sufficient to join a detached thread (in the sense of ensuring that the end of its execution has-happened-before the receiving of said signalling by a waiting thread), because, in general, there will be more code executed after e.g. a notify_all() of a condition variable, in particular the destructors of automatic and thread-local objects.
Running the signalling as the last thing the thread does (after destructors of automatic and thread-local objects has-happened) is what the _at_thread_exit family of functions was designed for.
So, in order to avoid undefined behaviour in the absence of any implementation guarantees above what the standard requires, you need to (manually) join a detached thread with an _at_thread_exit function doing the signalling or make the detached thread execute only code that would be safe for a signal handler, too.
Detaching Threads
According to std::thread::detach:
Separates the thread of execution from the thread object, allowing
execution to continue independently. Any allocated resources will be
freed once the thread exits.
From pthread_detach:
The pthread_detach() function shall indicate to the implementation
that storage for the thread can be reclaimed when that thread
terminates. If thread has not terminated, pthread_detach() shall not
cause it to terminate. The effect of multiple pthread_detach() calls
on the same target thread is unspecified.
Detaching threads is mainly for saving resources, in case the application does not need to wait for a thread to finish (e.g. daemons, which must run until process termination):
To free the application side handle: One can let a std::thread object go out of scope without joining, what normally leads to a call to std::terminate() on destruction.
To allow the OS to cleanup the thread specific resources (TCB) automatically as soon as the thread exits, because we explicitly specified, that we aren't interested in joining the thread later on, thus, one cannot join an already detached thread.
Killing Threads
The behavior on process termination is the same as the one for the main thread, which could at least catch some signals. Whether or not other threads can handle signals is not that important, as one could join or terminate other threads within the main thread's signal handler invocation. (Related question)
As already stated, any thread, whether detached or not, will die with its process on most OSes. The process itself can be terminated by raising a signal, by calling exit() or by returning from the main function. However, C++11 cannot and does not try to define the exact behaviour of the underlying OS, whereas the developers of a Java VM can surely abstract such differences to some extent. AFAIK, exotic process and threading models are usually found on ancient platforms (to which C++11 probably won't be ported) and various embedded systems, which could have a special and/or limited language library implementation and also limited language support.
Thread Support
If threads aren't supported std::thread::get_id() should return an invalid id (default constructed std::thread::id) as there's a plain process, which does not need a thread object to run and the constructor of a std::thread should throw a std::system_error. This is how I understand C++11 in conjunction with today's OSes. If there's an OS with threading support, which doesn't spawn a main thread in its processes, let me know.
Controlling Threads
If one needs to keep control over a thread for proper shutdown, one can do that by using sync primitives and/or some sort of flags. However, In this case, setting a shutdown flag followed by a join is the way I prefer, since there's no point in increasing complexity by detaching threads, as the resources would be freed at the same time anyway, where the few bytes of the std::thread object vs. higher complexity and possibly more sync primitives should be acceptable.
Consider the following code:
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
void thread_fn() {
std::this_thread::sleep_for (std::chrono::seconds(1));
std::cout << "Inside thread function\n";
}
int main()
{
std::thread t1(thread_fn);
t1.detach();
return 0;
}
Running it on a Linux system, the message from the thread_fn is never printed. The OS indeed cleans up thread_fn() as soon as main() exits. Replacing t1.detach() with t1.join() always prints the message as expected.
The fate of the thread after the program exits is undefined behavior. But a modern operating system will clean up all threads created by the process on closing it.
When detaching an std::thread, these three conditions will continue to hold:
*this no longer owns any thread
joinable() will always equal to false
get_id() will equal std::thread::id()
When the main thread (that is, the thread that runs the main() function) terminates, then the process terminates and all other threads stop.
Reference: https://stackoverflow.com/a/4667273/2194843
To allow other threads to continue execution, the main thread should terminate by calling pthread_exit() rather than exit(3).
It's fine to use pthread_exit in main. When pthread_exit is used, the main thread will stop executing and will remain in zombie(defunct) status until all other threads exit.
If you are using pthread_exit in main thread, cannot get return status of other threads and cannot do clean-up for other threads (could be done using pthread_join(3)). Also, it's better to detach threads(pthread_detach(3)) so that thread resources are automatically released on thread termination. The shared resources will not be released until all threads exit.
When the main process terminates all the worker threads created by that process are also killed. So, if the main() returns before a detached thread it created completes execution the detached thread will be killed by OS. Take this example:
void work(){
this_thread::sleep_for(chrono::seconds(2));
cout<<"Worker Thread Completed"<<endl;
}
int main(){
thread t(work);
t.detach();
cout<<"Main Returning..."<<endl;
return 0;
}
In the above program Worker Thread Completed will never be printed. Since main returns before the 2 second delay in the worker thread. Now if we change the code a little and add a delay greater than 2 seconds before main returns. Like:
void work(){
this_thread::sleep_for(chrono::seconds(2));
cout<<"Worker Thread Completed"<<endl;
}
int main(){
thread t(work);
t.detach();
cout<<"Main Returning..."<<endl;
this_thread::sleep_for(chrono::seconds(4));
return 0;
}
Output
Main Returning...
Worker Thread Completed
Now if a thread is created from any functions other than main the detached thread will stay alive until it's executions has completed even after the function returns. For example:
void child()
{
this_thread::sleep_for(chrono::seconds(2));
cout << "Worker Thread Completed" << endl;
}
void parent(){
thread t(child);
t.detach();
cout<<"Parent Returning...\n";
return;
}
int main()
{
parent();
cout<<"Main Waiting..."<<endl;
this_thread::sleep_for(chrono::seconds(5));
}
Output
Parent Returning...
Main Waiting...
Worker Thread Completed
A workaround to make main to wait for a detached worker thread before returning is to use condition_variable. For example:
#include <bits/stdc++.h>
using namespace std;
condition_variable cv;
mutex m;
void work(){
this_thread::sleep_for(chrono::seconds(2));
cout << "Worker Thread Completed" << endl;
cv.notify_all();
}
int main(){
thread t(work);
t.detach();
cout << "Main Returning..." << endl;
unique_lock<mutex>ul(m);
cv.wait(ul);
return 0;
}
I have tried searching many ways for the solution, but couldn't find proper one, so far.
I am using detached thread because I don't want my main thread to wait/block for the new child thread as it has many other important things to do.
I create a thread as follows:
std::thread rsync_t(&PreCompile::RunPreCompileThr, obj, arg1, arg2);
rsync_t.detach();
Now, Objective is to periodically check if this detached thread is active and running.
I tried future/promise and async way to do this, but it requires .get() which is something similar to join(), which I don't want.
Any suggestions to do this?
Thanks in advance.
Once you detach a thread, then you have explicitly said "I don't need to wait for this thread to finish". This is usually because the thread never finishes, and keeps running until the end of the program.
In any case, std::thread doesn't provide a mechanism to poll to see if a thread has finished without waiting. To do that you would need to use an alternative mechanism, whether the thread is detached or not.
One option is to start the thread with std::async(std::launch::async, func) and use the returned future to check if the thread is still running.
auto future=std::async(std::launch::async,thread_func);
bool thread_is_still_running=future.wait_for(std::chrono::seconds(0))!=std::future_status::ready;
If you use this option then you will need to keep the future object around (e.g. by storing it in a long-lived std::vector or a global variable), as its destructor will wait for the thread to finish.
Alternatively you can use a std::mutex and a boolean flag, or a std::atomic<bool> which is set from within the thread just before it exits, to indicate when the thread is done.
std::atomic<bool> done=false;
void thread_func(){
do_stuff();
done=true;
}
With std::async, you have an option to retrieve task status from the future. It is not necessary to use get().
https://en.cppreference.com/w/cpp/thread/future/wait_for
auto status = future.wait_for(std::chrono::milliseconds(1));
if (status == std::future_status::ready) {
// Thread has finished
}
If you detach a std::thread, you lose the communication channel that the std::thread object provides:
https://en.cppreference.com/w/cpp/thread/thread/detach
After calling detach *this no longer owns any thread.
If you want to communicate with the detached thread afterwards in any way, you need to do it manually. std::thread can no longer help you after detach.
I am using detached thread because I don't want my main thread to wait/block for the new child thread as it has many other important things to do.
The proper solution likely does not involve detach. You don't need to detach to have the thread run in parallel, it runs in parallel already when the std::thread constructor returns. Just keep the std::thread object alive and query through it, and only call join when the thread is actually supposed to be done/end. That said, std::thread only provides joinable which only changes after join, so it doesn't provide the information you need (that your code is "done" in some form).
I don't understand why when an std::thread is destructed it must be in join() or detach() state.
Join waits for the thread to finish, and detach doesn't.
It seems that there is some middle state which I'm not understanding.
Because my understanding is that join and detach are complementary: if I don't call join() than detach() is the default.
Put it this way, let's say you're writing a program that creates a thread and only later in the life of this thread you call join(), so up until you call join the thread was basically running as if it was detached, no?
Logically detach() should be the default behavior for threads because that is the definition of what threads are, they are parallelly executed irrespective of other threads.
So when the thread object gets destructed why is terminate() called? Why can't the standard simply treat the thread as being detached?
I'm not understanding the rationale behind terminating a program when either join() or detached() wasn't called before the thread was destructed. What is the purpose of this?
UPDATE:
I recently came across this. Anthony Williams states in his book, Concurrency In Action, "One of the proposals for C++17 was for a joining_thread class that would be similar to std::thread, except that it would automatically join in the destructor much like scoped_thread does. This didn’t get consensus in the committee, so it wasn’t accepted into the standard (though it’s still on track for C++20 as std::jthread)..."
Technically the answer is "because the spec says so" but that is an obtuse answer. We can't read the designers' minds, but here are some issues that may have contributed:
With POSIX pthreads, child threads must be joined after they have exited, or else they continue to occupy system resources (like a process table entry in the kernel). This is done via pthread_join().
Windows has a somewhat analogous issue if the process holds a HANDLE to the child thread; although Windows doesn't require a full join, the process must still call CloseHandle() to release its refcount on the thread.
Since std::thread is a cross-platform abstraction, it's constrained by the POSIX requirement which requires the join.
In theory the std::thread destructor could have called pthread_join() instead of throwing an exception, but that (subjectively) that may increase the risk of deadlock. Whereas a properly written program would know when to insert the join at a safe time.
See also:
https://en.wikipedia.org/wiki/Zombie_process
https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa
https://learn.microsoft.com/en-us/windows/win32/procthread/terminating-a-process
You're getting confused because you're conflating the std::thread object with the thread of execution it refers to. A std::thread object is a C++ object (a bunch of bytes in memory) that acts as a reference to a thread of execution. When you call std::thread::detach what happens is that the std::thread object is "detached" from the thread of execution -- it no longer refers to (any) thread of execution, and the thread of execution continues running independently. But the std::thread object still exists, until it is destroyed.
When a thread of execution completes, it stores its exit info into the std::thread object that refers to it, if there is one (If it was detached, then there isn't one, so the exit info is just thrown away.) It has no other effect on the std::thread object -- in particular the std::thread object is not destroyed and continues to exist until someone else destroys it.
You might want a thread to completely clean up after itself when it's done leaving no traces. This would mean that you could start a thread and then forget about it.
But you might also want to be able to manage a thread while it was running and get any return value it had provided when it was done. In this case, if a thread cleaned up after itself when it was done, your attempt to manage it could cause a crash because you would be accessing a handle that might be invalid. And to check for the return value when the thread finishes, the return value has to be stored somewhere, which means the thread can't be fully cleaned up because the place where the return value is stored has to be left around.
In most frameworks, by default, you get the second option. You can manage the thread (by interrupting it, sending signals to it, joining it, or whatever) but it can't clean up after itself. If you prefer the first option, there's a function to get that behavior (detach) but that means that you may not be able to access the thread because it may or may not continue to exist.
When a thread handle for an active thread goes out of scope you have a couple of options:
join
detach
kill thread
kill program
Each one of these options is terrible. No matter which one you pick it will be surprising, confusing and not what you wanted in most situations.
Arguably the joining thread you mentioned already exists in the form of std::async which gives you a std::future that blocks until the created thread is done, so doing an implicit join. But the many questions about why
std::async(std::launch::async, f);
g();
does not run f and g concurrently indicate how confusing that is. The best approach I'm aware of is to define it to be a programming error and have the programmer fix it, so an assert would be most appropriate. Unfortunately the standard went with std::terminate instead.
If you really want a detaching thread just write a little wrapper around std::thread that does if (thread.joinable()) thread.detach(); in its destructor or whichever handler you want.
Question: "So when the thread object gets destructed why is terminate() called? Why can't the standard simply treat the thread as being detached?"
Answer: Yes, I agree that it terminates the program badly but such design has its reasons. Without the std::terminate() mechanism in the destructor std::thread::~thread, if the users really wanted to do join(), but for some reason "join" didn't execute (for e.g. exception was thrown) then the new_thread will run in the background just like the detach() behaviors. This might cause undefined behaviors because that was not the original intention of the user to have a detached thread.
Assume I'm starting a std::thread and then detach() it, so the thread continues executing even though the std::thread that once represented it, goes out of scope.
Assume further that the program does not have a reliable protocol for joining the detached thread1, so the detached thread still runs when main() exits.
I cannot find anything in the standard (more precisely, in the N3797 C++14 draft), which describes what should happen, neither 1.10 nor 30.3 contain pertinent wording.
1 Another, probably equivalent, question is: "can a detached thread ever be joined again", because whatever protocol you're inventing to join, the signalling part would have to be done while the thread was still running, and the OS scheduler might decide to put the thread to sleep for an hour just after signalling was performed with no way for the receiving end to reliably detect that the thread actually finished.
If running out of main() with detached threads running is undefined behaviour, then any use of std::thread::detach() is undefined behaviour unless the main thread never exits2.
Thus, running out of main() with detached threads running must have defined effects. The question is: where (in the C++ standard, not POSIX, not OS docs, ...) are those effects defined.
2 A detached thread cannot be joined (in the sense of std::thread::join()). You can wait for results from detached threads (e.g. via a future from std::packaged_task, or by a counting semaphore or a flag and a condition variable), but that doesn't guarantee that the thread has finished executing. Indeed, unless you put the signalling part into the destructor of the first automatic object of the thread, there will, in general, be code (destructors) that run after the signalling code. If the OS schedules the main thread to consume the result and exit before the detached thread finishes running said destructors, what will^Wis defined to happen?
The answer to the original question "what happens to a detached thread when main() exits" is:
It continues running (because the standard doesn't say it is stopped), and that's well-defined, as long as it touches neither (automatic|thread_local) variables of other threads nor static objects.
This appears to be allowed to allow thread managers as static objects (note in [basic.start.term]/4 says as much, thanks to #dyp for the pointer).
Problems arise when the destruction of static objects has finished, because then execution enters a regime where only code allowed in signal handlers may execute ([basic.start.term]/1, 1st sentence). Of the C++ standard library, that is only the <atomic> library ([support.runtime]/9, 2nd sentence). In particular, that—in general—excludes condition_variable (it's implementation-defined whether that is save to use in a signal handler, because it's not part of <atomic>).
Unless you've unwound your stack at this point, it's hard to see how to avoid undefined behaviour.
The answer to the second question "can detached threads ever be joined again" is:
Yes, with the *_at_thread_exit family of functions (notify_all_at_thread_exit(), std::promise::set_value_at_thread_exit(), ...).
As noted in footnote [2] of the question, signalling a condition variable or a semaphore or an atomic counter is not sufficient to join a detached thread (in the sense of ensuring that the end of its execution has-happened-before the receiving of said signalling by a waiting thread), because, in general, there will be more code executed after e.g. a notify_all() of a condition variable, in particular the destructors of automatic and thread-local objects.
Running the signalling as the last thing the thread does (after destructors of automatic and thread-local objects has-happened) is what the _at_thread_exit family of functions was designed for.
So, in order to avoid undefined behaviour in the absence of any implementation guarantees above what the standard requires, you need to (manually) join a detached thread with an _at_thread_exit function doing the signalling or make the detached thread execute only code that would be safe for a signal handler, too.
Detaching Threads
According to std::thread::detach:
Separates the thread of execution from the thread object, allowing
execution to continue independently. Any allocated resources will be
freed once the thread exits.
From pthread_detach:
The pthread_detach() function shall indicate to the implementation
that storage for the thread can be reclaimed when that thread
terminates. If thread has not terminated, pthread_detach() shall not
cause it to terminate. The effect of multiple pthread_detach() calls
on the same target thread is unspecified.
Detaching threads is mainly for saving resources, in case the application does not need to wait for a thread to finish (e.g. daemons, which must run until process termination):
To free the application side handle: One can let a std::thread object go out of scope without joining, what normally leads to a call to std::terminate() on destruction.
To allow the OS to cleanup the thread specific resources (TCB) automatically as soon as the thread exits, because we explicitly specified, that we aren't interested in joining the thread later on, thus, one cannot join an already detached thread.
Killing Threads
The behavior on process termination is the same as the one for the main thread, which could at least catch some signals. Whether or not other threads can handle signals is not that important, as one could join or terminate other threads within the main thread's signal handler invocation. (Related question)
As already stated, any thread, whether detached or not, will die with its process on most OSes. The process itself can be terminated by raising a signal, by calling exit() or by returning from the main function. However, C++11 cannot and does not try to define the exact behaviour of the underlying OS, whereas the developers of a Java VM can surely abstract such differences to some extent. AFAIK, exotic process and threading models are usually found on ancient platforms (to which C++11 probably won't be ported) and various embedded systems, which could have a special and/or limited language library implementation and also limited language support.
Thread Support
If threads aren't supported std::thread::get_id() should return an invalid id (default constructed std::thread::id) as there's a plain process, which does not need a thread object to run and the constructor of a std::thread should throw a std::system_error. This is how I understand C++11 in conjunction with today's OSes. If there's an OS with threading support, which doesn't spawn a main thread in its processes, let me know.
Controlling Threads
If one needs to keep control over a thread for proper shutdown, one can do that by using sync primitives and/or some sort of flags. However, In this case, setting a shutdown flag followed by a join is the way I prefer, since there's no point in increasing complexity by detaching threads, as the resources would be freed at the same time anyway, where the few bytes of the std::thread object vs. higher complexity and possibly more sync primitives should be acceptable.
Consider the following code:
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
void thread_fn() {
std::this_thread::sleep_for (std::chrono::seconds(1));
std::cout << "Inside thread function\n";
}
int main()
{
std::thread t1(thread_fn);
t1.detach();
return 0;
}
Running it on a Linux system, the message from the thread_fn is never printed. The OS indeed cleans up thread_fn() as soon as main() exits. Replacing t1.detach() with t1.join() always prints the message as expected.
The fate of the thread after the program exits is undefined behavior. But a modern operating system will clean up all threads created by the process on closing it.
When detaching an std::thread, these three conditions will continue to hold:
*this no longer owns any thread
joinable() will always equal to false
get_id() will equal std::thread::id()
When the main thread (that is, the thread that runs the main() function) terminates, then the process terminates and all other threads stop.
Reference: https://stackoverflow.com/a/4667273/2194843
To allow other threads to continue execution, the main thread should terminate by calling pthread_exit() rather than exit(3).
It's fine to use pthread_exit in main. When pthread_exit is used, the main thread will stop executing and will remain in zombie(defunct) status until all other threads exit.
If you are using pthread_exit in main thread, cannot get return status of other threads and cannot do clean-up for other threads (could be done using pthread_join(3)). Also, it's better to detach threads(pthread_detach(3)) so that thread resources are automatically released on thread termination. The shared resources will not be released until all threads exit.
When the main process terminates all the worker threads created by that process are also killed. So, if the main() returns before a detached thread it created completes execution the detached thread will be killed by OS. Take this example:
void work(){
this_thread::sleep_for(chrono::seconds(2));
cout<<"Worker Thread Completed"<<endl;
}
int main(){
thread t(work);
t.detach();
cout<<"Main Returning..."<<endl;
return 0;
}
In the above program Worker Thread Completed will never be printed. Since main returns before the 2 second delay in the worker thread. Now if we change the code a little and add a delay greater than 2 seconds before main returns. Like:
void work(){
this_thread::sleep_for(chrono::seconds(2));
cout<<"Worker Thread Completed"<<endl;
}
int main(){
thread t(work);
t.detach();
cout<<"Main Returning..."<<endl;
this_thread::sleep_for(chrono::seconds(4));
return 0;
}
Output
Main Returning...
Worker Thread Completed
Now if a thread is created from any functions other than main the detached thread will stay alive until it's executions has completed even after the function returns. For example:
void child()
{
this_thread::sleep_for(chrono::seconds(2));
cout << "Worker Thread Completed" << endl;
}
void parent(){
thread t(child);
t.detach();
cout<<"Parent Returning...\n";
return;
}
int main()
{
parent();
cout<<"Main Waiting..."<<endl;
this_thread::sleep_for(chrono::seconds(5));
}
Output
Parent Returning...
Main Waiting...
Worker Thread Completed
A workaround to make main to wait for a detached worker thread before returning is to use condition_variable. For example:
#include <bits/stdc++.h>
using namespace std;
condition_variable cv;
mutex m;
void work(){
this_thread::sleep_for(chrono::seconds(2));
cout << "Worker Thread Completed" << endl;
cv.notify_all();
}
int main(){
thread t(work);
t.detach();
cout << "Main Returning..." << endl;
unique_lock<mutex>ul(m);
cv.wait(ul);
return 0;
}
According to the documentation here and here, the join method of a C++11 thread will throw a std::system_error if joinable() == false. Thus the natural way to wait for a thread to complete execution is something along the lines of:
if (thread2.joinable()) thread2.join();
However, this has the possibility to throw a std::system_error. Consider thread 1 calls thread2.joinable(), returns true, indicating that the thread2 is still running. Then the scheduler pauses thread1 and switches contexts to thread 2. Thread 2 completes, and then thread 1 resumes. Thread 1 calls thread2.join(), but thread2 has already completed, and as a result, std::system_error is thrown.
A possible solution is to wrap the whole thing in a try block:
try {
thread2.join();
catch (std::system_error &e) {}
But then when a legitimate std::system_error is thrown, possibly to indicate that the thread failed to join, the program continues on, acting as though everything is fine and dandy. Is there a proper way to join a thread besides using a try/catch block like this?
joinable does not do what you think it does. All it does is return whether the thread object is not associated with a thread. However, thread::join will fail if the thread object also represents the current thread. So the only reason for thread::join to fail due to lack of joinable is if you tried to join with yourself.
A completed thread (which isn't your own) is still perfectly joinable.