std::thread
A std::thread starts a function on another thread of execution. The new thread runs while the creator continues, and join waits for it to finish. The moment of the join is the boundary between the two, and forgetting it ends the program.
This lesson starts threads and waits for them, which is the whole of the interface. Sharing data between them is the next lesson's problem, because two threads on one variable without a lock is a data race even when it seems to work.
The thread object is a handle, not the thread itself: the thread continues after the handle is destroyed if it was detached, and the handle must be joined or detached before that.
Start and join
The constructor takes a callable and starts it on a new thread. join blocks the caller until that thread returns, and after the join the thread object is no longer joinable.
The program starts a thread that sets a captured variable to 42 and joins before printing it. The output is 42, and the read happens after the write because join orders them.
The captured variable is shared between the two threads, and the program is correct only because the read comes after the join. Without it the read and the write race, which is the subject of lesson 106.
#include <iostream>
#include <thread>
int main()
{
int value = 0;
std::thread worker{[&value] { value = 42; }};
worker.join();
std::cout << value << '\n';
}
g++ -std=c++23 -Wall -Wextra -pthread first.cpp -o first
./first
42
A function as the body
The thread can run a named function, and the arguments are copied into the thread's own storage. A reference parameter needs std::ref, because the thread constructor copies its arguments by default.
The program runs work on two numbers in one thread and prints the result. The function returns its value through a reference that std::ref binds to the local.
The copy is the reason a thread outlives the statement that started it: the arguments live in the thread object, not on the caller's stack. A pointer or a reference to a local is the caller's responsibility.
#include <iostream>
#include <thread>
void multiply(int a, int b, int& result)
{
result = a * b;
}
int main()
{
int result = 0;
std::thread worker{multiply, 6, 7, std::ref(result)};
worker.join();
std::cout << result << '\n';
}
g++ -std=c++23 -Wall -Wextra -pthread function.cpp -o function_thread
./function_thread
42
How many threads
hardware_concurrency() reports the number of threads the implementation suggests, usually the number of cores. It is a hint, not a promise, and a program should work when it returns zero.
The program prints the value, which is 4 on this machine, and the number of threads it would start. The number is the machine's, not the program's, so the page shows one machine's answer.
A program that splits work by this number gets a speedup on a machine with many cores and no slowdown on one with few. The thread pool of lesson 112 is the reusable form.
#include <iostream>
#include <thread>
int main()
{
unsigned int cores = std::thread::hardware_concurrency();
std::cout << cores << ' ' << (cores == 0 ? 1 : cores) << '\n';
}
g++ -std=c++23 -Wall -Wextra -pthread hardware.cpp -o hardware
./hardware
4 4
Two threads, one output
Two threads that print at the same time interleave their output. The program below starts two threads that each print three lines and joins both.
The output on this run shows the lines of one thread then the other, but the order is not something the program controls. Run it again and the lines may alternate, because the scheduler decides.
The output is shared state, and writing to std::cout from two threads without a lock is a data race in principle. The usual fix is to build the text in each thread and print it once, or to lock around the write.
#include <iostream>
#include <thread>
void report(const char* name)
{
for (int i = 0; i < 3; ++i) {
std::cout << name << ' ' << i << '\n';
}
}
int main()
{
std::thread first{report, "first"};
std::thread second{report, "second"};
first.join();
second.join();
}
g++ -std=c++23 -Wall -Wextra -pthread order.cpp -o order
./order
first 0
first 1
first 2
second 0
second 1
second 2
The thread that is never joined
A std::thread that is still joinable when it is destroyed calls std::terminate. The destructor has no way to wait, and detaching silently would be worse, so the standard makes it fatal.
The program starts a thread and lets the object go out of scope without join or detach. The runtime prints terminate called without an active exception and aborts, which is the message to look for when a program dies at the end of a function.
The fix is join when the program waits for the work, or detach when it does not need to. In modern C++ a std::jthread performs the join in its destructor, which lesson 114 covers.
#include <iostream>
#include <thread>
int main()
{
std::thread worker{[] { std::cout << "working\n"; }};
return 0;
}
g++ -std=c++23 -Wall -Wextra -pthread no_join.cpp -o no_join
gdb -q -batch -ex 'set disable-randomization off' -ex run -ex 'bt 3' ./no_join
[New LWP 2953]
working
terminate called without an active exception
[LWP 2953 exited]
Thread 1 "no_join" received signal SIGABRT, Aborted.
__restore_sigs (set=set@entry=0x7ffef0291cb0) at ./arch/x86_64/syscall_arch.h:40
warning: 40 ./arch/x86_64/syscall_arch.h: No such file or directory
#0 __restore_sigs (set=set@entry=0x7ffef0291cb0) at ./arch/x86_64/syscall_arch.h:40
#1 0x00007fa2871dd0cc in raise (sig=sig@entry=6) at src/signal/raise.c:11
#2 0x00007fa2871c069e in abort () at src/exit/abort.c:11
Exercises
- Start four threads that each print their index and join all of them.
- Pass a std::string by value to a thread and by reference with std::ref, and print it after the join.
- Call hardware_concurrency twice and compare the values.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
join()do?Answer
Waits for the thread to finish
join blocks until the thread function returns and then the thread is no longer joinable.
-
What happens when a joinable std::thread is destroyed?
Answer
The program calls std::terminate
The destructor cannot join, so the standard makes destroying a joinable thread fatal.
-
What does this program print?
int v = 0; std::thread t{[&v]{ v = 42; }}; t.join(); std::cout << v;Answer
42
The thread writes 42 and join orders the write before the read.
-
How does a thread take an argument by reference?
Answer
With std::ref
The constructor copies its arguments, so a reference has to be wrapped in std::ref.
-
What does
std::thread::hardware_concurrency()report?Answer
A hint at the number of hardware threads, possibly 0
It is a hint from the implementation and may be zero, so the program must handle that.
-
Which describes what this program may print?
#include <iostream> #include <thread> void report(const char* name) { for (int i = 0; i < 3; ++i) { std::cout << name << ' ' << i << '\n'; } } int main() { std::thread first{report, "first"}; std::thread second{report, "second"}; first.join(); second.join(); }Answer
The lines may interleave or come in either order
The scheduler decides when each thread runs, so the order of the prints is not fixed.
-
What does
detach()do?Answer
Lets the thread run independently of the thread object
After detach the thread object no longer refers to the running thread and its destructor is safe.
-
Why is reading v without a join a data race?
Answer
Because two threads access it and one writes without synchronisation
A read and a write to the same object without ordering is a data race, which is undefined behaviour.
-
What does the runtime print when a joinable thread is destroyed?
Answer
terminate called without an active exceptionThe terminate handler prints that line and aborts the process.
-
Which type joins its thread automatically in the destructor?
Answer
std::jthread
std::jthread from C++20 joins in its destructor and can also ask the thread to stop.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev