The only C++ application I now maintain used a single thread to handle background copying and it was fine, but anything more than that and it becomes a little more complex to maintain, especially when compared to threading in C#, such as TPL, Parallel library and async/await.
Now whilst these new features/libraries are not in the same “ease of use” area as those C# classes etc. they are nevertheless a massive step forward.
From what I can tell, we have four different multi-threaded operations (I think that’s the best way to put it). I’m not talking locking, or other forms of synchronization here.
std::thread
So at the most basic level we have the std::thread which is extremely simple to use, here’s an example
auto t = std::thread([] { // do something in a thread });
We can block (or wait) until the thread has completed using t.join();, but there’s other possibilities for interacting with our thread.
std::future and std::promise
Futures and promises are two sides of the same coin, so to speak. A promise allows us to return state from a thread. Whereas a future is for reading that returned state.
In other words, let’s think of it in this, fairly simple way – we create a promise that really says that at some time there will be a state change or return value from thread. The promise can be used to get a std::future, which is the object that gives us this return result.
So we’ve got the equivalent of a simple producer/consumer pattern
auto promise = std::promise<std::string>(); auto producer = std::thread([&] { // simulate some long-ish running task std::this_thread::sleep_for(std::chrono::seconds(5)); promise.set_value("Some Message"); }); auto future = promise.get_future(); auto consumer = std::thread([&] { std::cout << future.get().c_str(); }); // for testing, we'll block the current thread // until these have completed producer.join(); consumer.join();
Note: In the example above I’m simply using a lambda and passing reference to all variables in scope via the capture, using [&], as a capture list is required to access the promise and future outside of the lambdas.
In this example code we simply create a promise and then within the first thread we’re simulating a long-ish running task with the sleep_for method. Once the task is complete we set_value on the promise which causes the future.get() to unblock, but we’ve jumped ahead of ourselves, so…
Next we get the future (get_future) from the promise and to simulate a a producer/consumer we spin up another thread to handle any return from the promise via the future.
In the consumer thread, the call to future.get() will block that thread until the promise has been fulfilled.
Finally, the code calls producer.join() and consumer.join() to block the main thread until both producer and consumer threads have completed.
std::async
The example for the promise/future combination to create a producer consumer can be simplified further using the std::async which basically deals with creating a thread and creating a future for us. So let’s see the code for the producer/consumer now with the std::async
std::future<std::string> future = std::async([&] { std::this_thread::sleep_for(std::chrono::seconds(5)); return std::string("Some Message"); }); auto consumer = std::thread([&] { std::cout << future.get().c_str(); }); consumer.join();
In the above we’ve done away with the promise and the first thread which have both been replaced with the higher level construct, std::async. We’ve still got a future back and hence still call future.get() on it, but notice how we’ve also switch to returning the std::string and the future using the template argument to match the returned type.
We’re not actually sure if the code within the async lambda is run on a new thread or run on the calling thread, as the async documentation states…
“The template function async runs the function f asynchronously (potentially in a separate thread which may be part of a thread pool) and returns a std::future that will eventually hold the result of that function call.”
So the async method may run the code on a thread or deferred. Deferred meaning the code is run when we call the future’s get method in a “lazy” manner.
We can stipulate whether to run on a thread or as deferred using an std::async overload
std::future<std::string> future = std::async(std::launch::deferred, [&] { });
In the above we’ve deferred the async call and hence it’ll be run on the calling thread, alternatively we could stipulate std::launch::async, see std::launch.
What about exceptions?
If an exception occurs in async method or the promise set_exception is called in our producer/consumer using a promise. Then we need to catch the exception in the future (call to get()). Hence here’s an example of the async code with an exception occuring
std::future<std::string> future = std::async([&] { throw std::exception("Some Exception"); return std::string("Some Message"); }); auto consumer = std::thread([&] { try { std::cout << future.get().c_str(); } catch(std::exception e) { std::cout << e.what(); } }); consumer.join();
Here we throw an exception in the async lambda and when future.get is called, that exception is propagated through to the consumer thread. We need to catch it here and handle it otherwise it will leak through to the application and potentially crash the application if not handled elsewhere.