C/C++ Arena

Step 1 of 6

Threads and splitting work

Every program so far did one thing at a time: one line, then the next. A modern CPU has several cores, each able to run instructions at the same moment. A thread is one path of execution through your program. A program can start several threads, and the operating system runs them in parallel on different cores (or takes turns on one core). All threads of a program share the same memory: the same globals, the same heap, and any variable they're given a reference to.

Programs use threads to finish big jobs faster (four cores summing four quarters of the data), to keep working while waiting (a server handling one client while another is slow), and to keep an app responsive (the interface keeps drawing while a file loads).

A note about this module

The compiler in your browser runs programs as WebAssembly without thread support, so std::thread doesn't exist there. The examples in this module that start threads were compiled with GCC (-std=c++20 -pthread) and run on a Linux machine by the site's build, several times, and the output shown is what they printed every time. To run them yourself, save one as demo.cpp on a computer and build it with g++ -std=c++20 -pthread demo.cpp -o demo. The exercises practice the logic around threads, which works the same in the browser. The Pro Track's concurrency project then has you build real threaded code with ThreadSanitizer checking it.

Starting and joining threads

This program sums a million numbers with four threads, each summing its own quarter:

#include <iostream>
#include <numeric>
#include <thread>
#include <vector>

int main() {
    std::vector<int> data(1'000'000);
    std::iota(data.begin(), data.end(), 1);        // 1, 2, ..., 1000000

    const std::size_t parts = 4;
    std::vector<long long> partial(parts, 0);       // one result slot per thread
    std::vector<std::thread> workers;
    std::size_t chunk = data.size() / parts;

    for (std::size_t t = 0; t < parts; t++) {
        std::size_t lo = t * chunk;
        std::size_t hi = (t == parts - 1) ? data.size() : lo + chunk;
        workers.emplace_back([&data, &partial, t, lo, hi] {
            long long s = 0;
            for (std::size_t i = lo; i < hi; i++) s += data[i];
            partial[t] = s;                         // each thread writes only its own slot
        });
    }
    for (auto& w : workers) w.join();               // wait for all four to finish

    long long total = std::accumulate(partial.begin(), partial.end(), 0LL);
    std::cout << "total " << total << "\n";
}
total 500000500000

How it works

Splitting the work fairly

The example gives the last thread whatever is left over, so with 10 items and 3 threads the chunks are 3, 3 and 4. A better split spreads the remainder: 4, 3 and 3. The rule is that each part gets n / parts items, and the first n % parts parts get one extra. The parts are half-open ranges [lo, hi), like iterators: lo is included and hi is not, so an empty range has lo == hi.

Your turn: write split(n, parts) returning parts ranges that cover 0 to n in order, with sizes that differ by at most one (the first n % parts ranges get the extra item; parts is at least 1). Then write sum_range(v, lo, hi), the job each thread would run.

Next: Data races