: A crash course on the C++14 multi-threading constructs in a very non-verbose manner Summary The new C++ multi-threading constructs are very easy to learn. If you are familiar with C or C++ and want to start writing multithreaded programs, this article is for you! I use C++14 as a reference, but what I describe is also supported in C++17 . I only cover common constructs. You should be able to write your own multithreaded programs after reading this. Update (March 2020): I created a video on this subject. You can watch it here: Creating Threads A thread can be created in several ways: Using a function pointer Using a functor Using a lambda function These methods are very similar with minor differences. I explain each method and their differences next. Using a function pointer Consider the following function which takes a vector reference , a reference to the result , and two indices in the vector . The function adds all elements between and . v acm v beginIndex endIndex { acm = ; ( i = beginIndex; i < endIndex; ++i) { acm += v[i]; } } void accumulator_function2 ( :: < > &v, &acm, beginIndex, endIndex) const std vector int unsigned long long unsigned int unsigned int 0 for unsigned int A function calculating the sum of all elements between beginIndex and endIndex in a vector v Now lets say you want to partition the vector in two sections and calculate the total sum of each section in a separate thread and : t1 t2 { acm1 = ; acm2 = ; :: ; :: ; t1.join(); t2.join(); :: << << acm1 << ; :: << << acm2 << ; :: << << acm1 + acm2 << ; } //Pointer to function unsigned long long 0 unsigned long long 0 std thread t1 (accumulator_function2, ::ref(v), ::ref(acm1), , v.size() / ) std std 0 2 std thread t2 (accumulator_function2, ::ref(v), ::ref(acm2), v.size() / , v.size()) std std 2 std cout "acm1: " endl std cout "acm2: " endl std cout "acm1 + acm2: " endl Creating threads using function pointers What do you need to take away? creates a new thread. The first parameter is the name of the function pointer . Therefore, each thread will execute this function. std::thread accumulator_function2 The rest of the parameters passed to constructor are the parameters that we need to pass to . std::thread accumulator_function2 All parameters passed to are passed by value unless you wrap them in That’s why we wrapped , , and in . Important: accumulator_function2 std::ref. v acm1 acm2 std::ref Threads created by do not have return values. If you want to return something, you should store it in one of the parameters passed by reference, i.e. . std::thread acm Each thread starts as soon as it gets created. We use function to wait for a thread to finish join() Using Functors You can do exactly the same thing using functors. The following is the code that uses a functor: : { _acm = ; ( i = beginIndex; i < endIndex; ++i) { _acm += v[i]; } } _acm; }; { class CAccumulatorFunctor3 public void operator () ( :: < > &v, beginIndex, endIndex) const std vector int unsigned int unsigned int 0 for unsigned int unsigned long long Functor Definition And the code that creates the threads is: { CAccumulatorFunctor3 accumulator1 = CAccumulatorFunctor3(); CAccumulatorFunctor3 accumulator2 = CAccumulatorFunctor3(); :: ; :: ; t1.join(); t2.join(); :: << << accumulator1._acm << ; :: << << accumulator2._acm << ; :: << << accumulator1._acm + accumulator2._acm << ; } //Creating Thread using Functor std thread t1 ( ::ref(accumulator1), ::ref(v), , v.size() / ) std std 0 2 std thread t2 ( ::ref(accumulator2), ::ref(v), v.size() / , v.size()) std std 2 std cout "acm1: " endl std cout "acm2: " endl std cout "accumulator1._acm + accumulator2._acm : " endl Creating threads using functors What do you need to take away? Everything is very similar to function pointer, except that: The first parameter is the functor object. Instead of passing a reference to the functor to store the result, we can store its return value in a member variable inside the functor, i.e. in . _acm Using Lambda Functions As the third alternative we can define each thread in a lambda function as shown below: { acm1 = ; acm2 = ; :: ; :: ; t1.join(); t2.join(); :: << << acm1 << ; :: << << acm2 << ; :: << << acm1 + acm2 << ; } unsigned long long 0 unsigned long long 0 std thread t1 ([&acm1, &v] { ( i = ; i < v.size() / ; ++i) { acm1 += v[i]; } }) for unsigned int 0 2 std thread t2 ([&acm2, &v] { ( i = v.size() / ; i < v.size(); ++i) { acm2 += v[i]; } }) for unsigned int 2 std cout "acm1: " endl std cout "acm2: " endl std cout "acm1 + acm2: " endl Creating threads using lambda functions Again, everything is very similar to function pointer, except that: As an alternative to pass a parameter, we can pass references to lambda functions using lambda capture. Tasks, Futures, and Promises As an alternative to , you can use tasks. std::thread Tasks work very similar to threads, but the main difference is that they can return a value. So, you can remember them as a more abstract way of defining your threads and use them when the threads return a value. Below is the same example written using tasks: { f1 = []( :: < > &v, left, right) { acm = ; ( i = left; i < right; ++i) { acm += v[i]; } acm; }; t1 = ::async(f1, ::ref(v), , v.size() / ); t2 = ::async(f1, ::ref(v), v.size() / , v.size()); acm1 = t1.get(); acm2 = t2.get(); :: << << acm1 << ; :: << << acm2 << ; :: << << acm1 + acm2 << ; } # include <future> //Tasks, Future, and Promises auto std vector int unsigned int unsigned int unsigned long long 0 for unsigned int return auto std std 0 2 auto std std 2 //You can do other things here! unsigned long long unsigned long long std cout "acm1: " endl std cout "acm2: " endl std cout "acm1 + acm2: " endl What do you need to take away? Tasks are defined and created using , (instead of threads that are created using ) std::async std::thread The returned value from is called a . Don’t get scared by its name. It just means and are variables whose value will be assigned to in the future. We get their values by calling and std::async std::future t1 t2 t1.get() t2.get() If the future values are not ready, upon calling the main thread blocks until the future value becomes ready (similar to ). get() join() Notice that the function that we passed to returns a value. This value is passed through a type called std::promise. Again, don’t get scared by its name. For the most part, you don’t need to know details of or define any variable of type . The C++ library does that behind the scenes. std::async std::promise std::promise Each task by default starts as soon as it is created (there is a way to change this which I don’t cover). Summary of Creating Threads There you have it. Creating threads is as simple as what I explained above. You can either use : std::thread Use function pointers Use functors Use lambda functions Or you can use to create a and get the return values in a . Tasks can get also use a function pointer, a functor, or a lambda function. std::async task std::future Shared Memory and Shared Resources In short, threads should be careful when they read/write into shared memory and resources (such as files) to avoid race conditions. C++14 provides several constructs to synchronize threads to avoid such race conditions. Using Mutex, lock,() and unlock() (Not recommended) The following code shows how we create a critical section such that each thread accesses exclusively: std::cout ::mutex g_display_mutex; thread_function() { g_display_mutex.lock(); ::thread::id this_id = ::this_thread::get_id(); :: << << this_id << ; g_display_mutex.unlock(); } std std std std cout "My thread id is: " endl What do you need to take away? A mutex is created std::mutex A critical section (i.e. guaranteed to be run only by a single thread at each time) is created using lock() The critical section ends upon calling unlock() Each thread waits at and only enters the critical section if no other thread is inside that section. lock() While the above method works, it is not recommended because: It is not exception safe: if the code before lock generates an exception, will not be executed, and we never release the mutex which might cause deadlock unlock() We always have to be careful not to forget to call unlock() Using std::lock_guard (recommended) Don’t get scared by its name . It’s just a more abstract way of creating critical sections. lock_guard Below is the same critical section using lock_guard: ::mutex g_display_mutex; thread_function() { ::lock_guard< ::mutex> guard(g_display_mutex); ::thread::id this_id = ::this_thread::get_id(); :: << << this_id << ; } std std std std std std cout "From thread " endl critical section using lock_guard What do you need to take away? The code coming after std::lock_guard creation is automatically locked. No need for explicit and function calls. lock() unlock() The critical section automatically ends when goes out of scope. This makes it exception safe, and also we don’t need to remember to call std::lock_guard unlock() still requires using a variable of type in its constructor. lock_guard std::mutex How Many Threads Should We Create? You can create as many threads as you want, but it would probably be pointless if the number of active threads is more than the number of available CPU cores. In order to get the maximum number of cores you can call: as shown below: std::thread::hardware_cuncurrency() { c = ::thread::hardware_concurrency(); :: << << c << ;; } unsigned int std std cout " number of cores: " endl What I Didn’t Cover I covered most of what you need to create threads. There are several other details that are less common which I don’t include here, but you can study them on your own: std::move details of std::promise std::packaged_task Conditional variables Hope this helps you learning C++ multi threading quickly. If you liked this article please click on the clap and give me feedback.