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
std::thread(from<thread>) starts running the function you give it immediately, alongsidemain. Here each thread runs a lambda.workers.emplace_back(...)constructs the thread directly inside the vector.- The capture list matters.
dataandpartialare captured by reference because the threads must work on the real vectors.t,loandhiare captured by copy: the loop changes them while the threads are still running, so a reference to them would read whatever value the loop had reached by then. join()waits until that thread has finished. After the joins, main can safely read everything the threads wrote.- Every
std::threadmust be joined (or detached, which lets it run on unsupervised) before thestd::threadobject is destroyed. Otherwise the program callsstd::terminateand dies. C++20'sstd::jthreadjoins automatically in its destructor, which is RAII again. - The threads write to different elements of
partial, which is safe: different elements are different memory locations. If they all added into one sharedlong long total, that would be a data race, the subject of the next step. - Threads finish in an unpredictable order. That's why the program only prints after joining.
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.