Exception safety means an operation handles failure without leaking resources, breaking invariants, introducing races, or unexpectedly terminating the program. In sequential code, cleanup can usually happen as an exception travels back to the caller. Concurrent code has an additional problem: an exception escaping a function running on another thread does not travel to the thread that started it; it terminates the application. section 8.4 section 8.4.1
A throwing parallel accumulation
Suppose parallel_accumulate starts several std::thread objects, then computes the final block on the calling thread. If starting a later thread or computing that block throws, unwinding destroys earlier, still-joinable thread objects; their destructors call std::terminate. The same cleanup issue arises when ordinary work throws between starting a thread and joining it. Identify potentially throwing operations both before and after threads are started, including operations on user-supplied types. section 8.4.1 section 2.1.3
Separate cleanup from the handling of worker failures:
- Arrange for every started thread to be joined on both normal and exceptional exits. A try/catch can join and rethrow, but a RAII guard makes joining automatic during unwinding. Check joinable() before joining. section 8.4.1 section 2.1.3
- Do not rely on joining to transport an exception from a worker thread. A std::packaged_task paired with a future provides a way to collect its result or exception on the calling thread; an std::async-based accumulation uses futures for its recursively computed halves. section 8.4.1
- If waiting is unnecessary, detaching removes the association with the std::thread object and avoids termination when that object is destroyed, but leaves the thread running in the background. section 2.1.3
Listing 8.5. An exception-safe parallel version of std::accumulate using std::async
template<typename Iterator,typename T>
T parallel_accumulate(Iterator first,Iterator last,T init)
{
unsigned long const length=std::distance(first,last); #1
unsigned long const max_chunk_size=25;
if(length<=max_chunk_size)
{
return std::accumulate(first,last,init); #2
}
else
{
Iterator mid_point=first;
std::advance(mid_point,length/2); #3
std::future<T> first_half_result=
std::async(parallel_accumulate<Iterator,T>, #4
first,mid_point,init);
T second_half_result=parallel_accumulate(mid_point,last,T()); #5
return first_half_result.get()+second_half_result; #6
}
}
!@%STYLE%@!
{"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":3,\"ch\":71},{\"line\":3,\"ch\":73}],[{\"line\":7,\"ch\":70},{\"line\":7,\"ch\":72}],[{\"line\":12,\"ch\":70},{\"line\":12,\"ch\":72}],[{\"line\":14,\"ch\":70},{\"line\":14,\"ch\":72}],[{\"line\":16,\"ch\":70},{\"line\":16,\"ch\":72}],[{\"line\":17,\"ch\":70},{\"line\":17,\"ch\":72}]]"}
!@%STYLE%@!
The recursive parallel_accumulate computes one half through std::async and the other on the calling thread, then obtains the first half’s result through its future. section 8.4.1
Exception safety also shapes concurrent data structures. In a condition-variable queue, notify_one() may wake a waiter that then throws while constructing a result, leaving other waiters asleep despite an available entry. Options include notifying all waiters, notifying another waiter after the exception, or storing shared pointers created during push() so retrieving one does not throw. Notifying all can wake threads that immediately find the queue empty and return to waiting. section 6.2.2
A lock-free pop that loses a value
A lock-free stack must claim a node before safely copying its data. If that copy throws after removal, the value is lost; passing an output reference does not fix the ordering. Allocating a shared pointer when pushing lets pop() return that pointer without a throwing value copy. This addresses the lost-value problem, not the separate node leak in the illustrated stack. section 7.2.1
