Multithreading

Sometimes we want our computer to perform multiple tasks at the same time. For that, we can use multithreading. We simply give separate “threads” their own functions to perform, and our computer will attempt to run all those functions at once. To use threads in C++, we first must #include <thread>.

The program below creates two thread objects: thread1 and thread2, constructed with the backgroundTask1 and backgroundTask2 functions, respectively:

#include <iostream>
#include <thread>

void backgroundTask1() {
    std::cout << "Beginning background task 1...\n";
    for (int i = 0; i < 20; i++) {
        std::cout << "Background task 1: i = " << i << "\n";
    }
    std::cout << "Background task 1 completed!\n";
}

void backgroundTask2() {
    std::cout << "Beginning background task 2...\n";
    for (int i = 0; i < 20; i++) {
        std::cout << "Background task 2: i = " << i << "\n";
    }
    std::cout << "Background task 2 completed!\n";
}

int main() {
    std::thread thread1(backgroundTask1);
    std::thread thread2(backgroundTask2);

    thread1.join();
    thread2.join();

    return 0;
}

Sample Program Output (may vary):

Beginning background task 1...
Background task 1: i = 0
Background task 1: i = 1
Background task 1: i = 2
Background task 1: i = 3
Background task 1: i = 4
Background task 1: i = 5
Background task 1: i = 6
Background task 1: i = 7
Background task 1: i = 8
Background task 1: i = 9Beginning background task 2...
Background task 2: i = 0
Background task 2: i = 1
Background task 2: i = 2
Background task 2: i = 3
Background task 2: i = 4
Background task 2: i = 5
Background task 2: i = 6
Background task 2: i = 7
Background task 2: i = 8
Background task 2: i = 9
Background task 2: i = 10
Background task 2: i = 11
Background task 2: i = 12
Background task 2: i = 13
Background task 2: i = 14
Background task 2: i = 15
Background task 2: i = 16
Background task 2: i = 17
Background task 2: i = 18
Background task 2: i = 19
Background task 2 completed!

Background task 1: i = 10
Background task 1: i = 11
Background task 1: i = 12
Background task 1: i = 13
Background task 1: i = 14
Background task 1: i = 15
Background task 1: i = 16
Background task 1: i = 17
Background task 1: i = 18
Background task 1: i = 19
Background task 1 completed!

Output

Because backgroundTask1 and backgroundTask2 run at the same time, the program’s output looks a bit messy. For example, “Beginning background task 2…” was printed before std::endl was added to “Background task 1: i = 9”. Also, because the two functions are running together, the output alternates between background tasks 1 and 2.

Thread Constructors

After creating a thread object, the function passed to its constructor automatically starts running. The constructor can also be given additional arguments representing arguments for the function to run. Here’s what this could look like, giving backgroundTask1 an int parameter:

#include <iostream>
#include <thread>

void backgroundTask1(int max) {
    std::cout << "Beginning background task 1...\n";
    for (int i = 0; i < max; i++) {
        std::cout << "Background task 1: i = " << i << "\n";
    }
    std::cout << "Background task 1 completed!\n";
}

void backgroundTask2() {
    std::cout << "Beginning background task 2...\n";
    for (int i = 0; i < 20; i++) {
        std::cout << "Background task 2: i = " << i << "\n";
    }
    std::cout << "Background task 2 completed!\n";
}

int main() {
    std::thread thread1(backgroundTask1, 20);
    std::thread thread2(backgroundTask2);

    thread1.join();
    thread2.join();

    return 0;
}

Note: In the thread constructor, we actually are passing a function pointer. Function pointers are indicated by just typing the name of the function (rather than the function name followed by two parentheses).

Joining Threads

At the end of main, we call the join function of each thread. This just causes the computer to wait for both threads to finish before returning 0 (and exiting the program). Without doing this, likely neither of the threads would have time to finish running their assigned functions, causing the program’s output to look something like this:

Beginning background task 1...
Background task 1: i = 0
Background task 1: i = 1
Background task 1: i = 2
Background task 1: i = 3
Background task 1: i = 4
Background task 1: i = 5
Background task 1: i = 6
Background task 1: i = 7
Background task 1: i = 8
Background task 1: i = t9erminate called without an active exception

The opposite of joining a thread is detaching (using the detach function), which causes the thread to act independently, preventing the computer from waiting for it to finish. Although this behaves just like not calling join, always be sure to either call the join or detach function at some point. as not doing so can prevent important destructors from being called.

The Main Thread

The “main” thread in a program is the thread that runs the main function. Basically, when not multithreading, everything we do happens on the main thread.

Sleeping

A delay can be added in any thread (including the program’s main thread) by using the std::this_thread::sleep_for function (when using it, be sure to include <chrono>):

#include <iostream>
#include <thread>
#include <chrono>

int main() {
    std::cout << "Wainting 5 seconds...\n";
    std::this_thread::sleep_for(std::chrono::seconds(5));
    std::cout << "5 seconds have passed.\n";
    return 0;
}

If both std::cout messages appear at the same time for you, you may need to use std::endl instead of "\n". When using std::cout, any text you want to display is placed on a queue (a “buffer”) that’s automatically sent to your computer’s screen (“flushed”) at the end of the program. (For some computers, like mine, flushing automatically occurs more often than this, however.) std::endl not only adds a new line to program output, but also flushes these output buffers. Therefore, only using "\n" can sometimes cause all text to be displayed at once when the program finishes.

For me this wasn’t a problem, but you can try out the program below if it was:

#include <iostream>
#include <thread>
#include <chrono>

int main() {
    std::cout << "Wainting 5 seconds..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(5));
    std::cout << "5 seconds have passed." << std::endl;
    return 0;
}
Time Units

The supported units for the sleep_for function are nanoseconds, microseconds, milliseconds, seconds, minutes, and hours, that can all be used just like std::chrono::seconds.

Also, by using namespace std::chrono_literals;, we can use standard abbreviations for these units (ns, us, ms, s, min, and h respectively):

#include <iostream>
#include <thread>
#include <chrono>

int main() {
    using namespace std::chrono_literals;

    std::cout << "Wainting 5 seconds...\n";
    std::this_thread::sleep_for(5s);
    std::cout << "5 seconds have passed.\n";
    
    return 0;
}

Each of these abbreviated units is actually just an overloaded operator that returns an std::chrono::duration type:

  • The “ns” (for nanoseconds) overloads operator""ns.
  • The “us” (for microseconds) overloads operator""us.
  • And so on…

Challenge Problem

Create a thread that finds (and prints) the sum of every number from 0 to 1,000,000,000 using a for/while loop. Hint: An int may not be big enough to hold this sum.

While the sum is being calculated, print “Calculating…” every 100 milliseconds, and print “Done!” when the program finishes. Do this on the program’s main thread.

Bonus: Instead of “Calculating…”, display the calculation’s progress as a percentage every 100 milliseconds.

If you get stuck, check out my solution here.