Handling C++ exceptions thrown from worker thread in the main thread

Introduction

Before C++0x (C++98 or C++03), an exception is a thread local object: it must be handled in the same thread where it was thrown. Any other thread cannot handle the exception using a catch block. If you write a multi-threaded program, and you have an exception in one of the threads, you have to handle it locally. If it has to be communicated to another thread, e.g., the main thread, the information of the exception must be encoded as an error code, or a descriptive string etc., and then sent to the target thread. The target thread must decode the information out of the error code or string and process it accordingly. This makes exception handling unnatural, and the code is error prone and hard to maintain.

In C++0x (x is now hex for sure), exceptions are allowed to be moved from one thread to another thread as a library feature (N2179: Language Support for Transporting Exceptions between Threads). Exceptions can be handled in a thread other than the throwing thread, for example, the main thread or a dedicated exception handling thread. Therefore the code is more logical and maintainable.

Example

Below is an example illustrating this feature.

  1 class ExceptionLocal {};		// exception handled locally within thread
  2 class ExceptionTransferred {};	// exception to be transferred across threads
  3 
  4 std::vector<std::exception_ptr> g_exceptions_transferred; // global "references" to thrown exceptions
  5 std::mutex g_mutex;	// mutex to protect the above exceptions vector
  6 
  7 void thread_func()
  8 {	// the secondary thread starts here
  9 	try
 10 	{
 11 		// User code: do the real work. 
 12 		// This part of code can be unaware of exception transport, 
 13 		// just throw exceptions at wish and let catch blocks handle them.
 14 		// ...
 15 		if( local_exception_condition )
 16 			throw ExceptionLocal();
 17 		if( exception_to_be_transferred_condition )
 18 			throw ExceptionTransferred();
 19 
 20 		// However, if you know an exception is to be transported, 
 21 		// you can skip the normal try-catch flow for efficiency:
 22 		if( exception_to_be_transferred_condition )
 23 		{
 24 			// create exception for transport.
 25 			std::exception_ptr ep = std::copy_exception(ExceptionTransferred());	
 26 			// obtain exclusive access to g_exceptions_transferred
 27 			std::lock_guard<std::mutex> lock(g_mutex);
 28 			// store in global exception references
 29 			g_exceptions_transferred.push_back( ep );
 30 			return;	// implicitly terminate this thread.
 31 		}
 32 	}
 33 	catch(const ExceptionLocal& e)
 34 	{	// handle the local exception locally
 35 		// ...
 36 	}
 37 	catch(const ExceptionTransferred& e)
 38 	{	// exception to be transferred to another thread caught
 39 		// obtain exclusive access to g_exceptions_transferred
 40 		std::lock_guard<std::mutex> lock(g_mutex);	
 41 		// store in global exception references
 42 		g_exceptions_transferred.push_back(std::current_exception());
 43 		return;	// implicitly terminate this thread.
 44 	}
 45 	return;	// normal thread exit
 46 }
 47 
 48 int main(int argc, char* argv[])
 49 {
 50 	g_exceptions_transferred.clear();	// establish no exception state
 51 	
 52 	// create two secondary working threads
 53 	std::thread t1(thread_func);
 54 	std::thread t2(thread_func);
 55 	// wait for the two threads to finish
 56 	t1.join();
 57 	t2.join();
 58 
 59 	// handle the transferred exceptions if any
 60 	for(const std::exception_ptr& ep : g_exceptions_transferred)
 61 	{
 62 		try
 63 		{
 64 			if( ep!=nullptr )
 65 				std::rethrow_exception(ep);
 66 		}
 67 		catch(const ExceptionTransferred& e)
 68 		{	// real place to handle the transferred exception
 69 			// ...
 70 		}
 71 	}
 72 
 73 	return 0;
 74 } 

In the example above, a few C++0x features are used. They are explained along with the example.

Line 1 and 2 define two hypothetical exception classes that the secondary worker thread can throw. ExceptionLocal is supposed to be handled by the worker thread locally. ExceptionTransferred represents an exception that should be communicated to the main thread for handling.

Line 4 defines a global vector that stores the possibly multiple exceptions transferred from the multiple worker threads to the main thread. Line 5 defines a std::mutex (new feature in C++0x threading library, see also std::thread) that is used to protect the global vector.

Lines 7 through 46 define the worker thread function. The code is clearly commented and easy to follow.

Line 25 cooks a std::exception_ptr object directly using std::copy_exception for transfer, instead of going through the throw-catch-std::current_exception. The latter is slower and less efficient, however, it is more general because the actual code is unaware of the exception transport and therefore can run when no other thread is going to handle the exception.

Line 27 and 40 use std::lock_guard (new feature in C++0x threading library, see also std::mutex) to obtain the mutex; the mutex is released when the lock guard destructs. The sole purpose of the global mutex is for threads to gain exclusive access to the global exception reference vector.

Line 29 and 42 simply store the new exception in the global vector of std::exception_ptr. An exception_ptr object represents a reference to the actual exception that was thrown and not yet handled. In this example, the worker threads would terminate after it stores the exception reference, and the main thread waits until all worker threads finish and then inspects the transferred exceptions. In practice, any worker thread can also signal the main thread about exception happening after it stores the exception reference, and the main thread can handle the exception immediately.

Line 48 through 74 define the main thread function. Line 53 and 54 create two worker threads using std::thread, in new C++0x thread library N2320: Multi-threading Library for Standard C++. They run immediately. The main thread will not block; to wait until the two worker threads finish, it calls the join function. The main thread then checks the global exception reference vector.

Line 64 shows a substitue for NULL or 0, nullptr, a new C++0x feature (N2431: A name for the null pointer: nullptr) that is dedicated for null pointer constant and does not have side effects like a integer constant.

Line 65 calls std::rethrow_exception, which knows what type of exception to throw from an exception reference, in this example, ExceptionTransferred. Line 67 then catches the normal C++ exception ExceptionTransferred and handles it in the main thread. Note that all different types of exceptions can be conveyed by std::exception_ptr, and std::rethrow_exception can recover them exactly.

Conclusion

In summary, the worker thread uses std::current_exception or std::copy_exception to obtain an exception reference object of class std::exception_ptr, stores it in a location that the main thread can access. The main thread calls std::rethrow_exception to emit the original C++ exception, and handles it just like that the exception was thrown within the thread.

A simple tutorial about C++0x multiple threading support is available here: Simpler Multithreading in C++0x.

P.S.: Check also Transporting of Exceptions Between Threads (the Boost approach).

About these ads

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 40 other followers

%d bloggers like this: