Ready to practice?
Sign up to access interactive coding exercises and track your progress.
std::thread Basics
Learn how to create and manage threads using std::thread.
Creating Your First Thread
The std::thread class, defined in <thread>, represents a single thread of execution. To create a thread, you construct a std::thread object with a callable (function, lambda, or functor) to execute.
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello from thread!\n";
}
int main() {
std::thread t(hello); // Create thread, starts running immediately
t.join(); // Wait for thread to complete
return 0;
}
When you construct a std::thread with a callable, the new thread starts executing immediately. It doesn't wait for you to call any "start" method.
The Callable Can Be Anything
std::thread accepts any callable: regular functions, lambdas, function objects, or member functions.
Regular Functions
void do_work() {
std::cout << "Working...\n";
}
int main() {
std::thread t(do_work);
t.join();
}
Lambda Expressions
Lambdas are often the most convenient option:
int main() {
std::thread t([]() {
std::cout << "Lambda thread!\n";
});
t.join();
}
Function Objects (Functors)
A class with operator() defined:
class Worker {
public:
void operator()() const {
std::cout << "Functor thread!\n";
}
};
int main() {
Worker w;
std::thread t(w); // Passes a copy of w
t.join();
}
Watch out for the "most vexing parse" with temporary functors:
// WRONG: Declares a function named t, doesn't create a thread!
std::thread t(Worker());
// Correct alternatives:
std::thread t{Worker()}; // Braced initialization
std::thread t((Worker())); // Extra parentheses
std::thread t(Worker{}); // Braced temporary
Member Functions
To call a member function on a thread, pass a pointer to the member function and the object:
class Printer {
public:
void print_message() const {
std::cout << "Member function thread!\n";
}
};
int main() {
Printer p;
std::thread t(&Printer::print_message, &p); // Pass pointer to object
t.join();
}
The &p is required because member functions need a this pointer. You can also pass a reference wrapper or a smart pointer.
You Must Join or Detach
Every std::thread object must be either joined or detached before it is destroyed. If you destroy a std::thread that is still "joinable," the program calls std::terminate() and aborts.
void work() { /* ... */ }
int main() {
std::thread t(work);
// Forgot to join or detach!
// When t is destroyed here, std::terminate() is called
} // CRASH!
join() - Wait for Completion
Calling join() blocks the calling thread until the thread represented by the std::thread object completes:
std::thread t(work);
// ... do other things ...
t.join(); // Wait here until t finishes
// Now we know the thread is done
After calling join(), the thread is no longer joinable:
std::thread t(work);
t.join();
std::cout << t.joinable() << '\n'; // Prints 0 (false)
t.join(); // ERROR: Can't join a non-joinable thread
detach() - Fire and Forget
Calling detach() separates the thread of execution from the std::thread object. The thread continues running independently, and you lose the ability to wait for it:
std::thread t(work);
t.detach(); // Thread runs in the background
// t is now empty (not joinable)
// We can't wait for the thread anymore
Detached threads are sometimes called daemon threads. They're useful for background tasks that should run for the program's lifetime.
Detached threads are dangerous! If the main thread exits while a detached thread is still running, the detached thread is abruptly terminated. Any resources it's using may be left in an inconsistent state.
void use_local_variable() {
int local = 42;
std::thread t([&local]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << local << '\n'; // DANGER: local may be destroyed!
});
t.detach();
} // local is destroyed here, but thread still references it!
Checking if a Thread is Joinable
Use joinable() to check if a thread can be joined or detached:
std::thread t;
std::cout << t.joinable() << '\n'; // 0 (default-constructed, empty)
t = std::thread(work);
std::cout << t.joinable() << '\n'; // 1 (has a running thread)
t.join();
std::cout << t.joinable() << '\n'; // 0 (already joined)
A thread is not joinable if:
- It was default-constructed (no thread)
- It has been moved from
- It has been joined or detached
Moving Threads
std::thread is movable but not copyable. This makes sense - a thread represents a unique system resource.
std::thread t1(work);
// std::thread t2 = t1; // ERROR: Can't copy
std::thread t2 = std::move(t1); // OK: Move ownership
// t1 is now empty (not joinable)
// t2 owns the thread
t2.join();
Moving is useful for storing threads in containers or returning them from functions:
std::thread create_thread() {
return std::thread(work); // Implicit move
}
int main() {
std::thread t = create_thread();
t.join();
}
Managing Multiple Threads
For multiple threads, use a container:
#include <vector>
#include <thread>
void task(int id) {
std::cout << "Thread " << id << " running\n";
}
int main() {
std::vector<std::thread> threads;
// Create 10 threads
for (int i = 0; i < 10; ++i) {
threads.emplace_back(task, i); // Construct in place with argument
}
// Join all threads
for (auto& t : threads) {
t.join();
}
}
Use `emplace_back` rather than `push_back` to construct threads directly in the vector, avoiding unnecessary moves.
Exception Safety with RAII
If an exception is thrown between thread creation and join(), the thread will be destroyed without being joined, causing termination:
void risky() {
std::thread t(work);
do_something_that_might_throw(); // If this throws...
t.join(); // ...we never reach here!
} // t destroyed while joinable -> terminate()!
The solution is a RAII wrapper that joins in its destructor:
class ThreadGuard {
std::thread& t_;
public:
explicit ThreadGuard(std::thread& t) : t_(t) {}
~ThreadGuard() {
if (t_.joinable()) {
t_.join();
}
}
ThreadGuard(const ThreadGuard&) = delete;
ThreadGuard& operator=(const ThreadGuard&) = delete;
};
void safe() {
std::thread t(work);
ThreadGuard guard(t); // Will join in destructor
do_something_that_might_throw(); // Even if this throws...
// ...guard's destructor will join t
}
C++20 introduced `std::jthread`, which handles this automatically. We'll cover it later in this chapter.
A Complete Example
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
void count_to(int n, const std::string& name) {
for (int i = 1; i <= n; ++i) {
std::cout << name << ": " << i << '\n';
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main() {
std::cout << "Main: Starting threads\n";
std::thread alice(count_to, 5, "Alice");
std::thread bob(count_to, 3, "Bob");
std::cout << "Main: Threads started, waiting...\n";
alice.join();
std::cout << "Main: Alice finished\n";
bob.join();
std::cout << "Main: Bob finished\n";
std::cout << "Main: All done!\n";
return 0;
}
Possible output (order varies due to scheduling):
Main: Starting threads
Main: Threads started, waiting...
Alice: 1
Bob: 1
Alice: 2
Bob: 2
Alice: 3
Bob: 3
Main: Bob finished
Alice: 4
Alice: 5
Main: Alice finished
Main: All done!
std::thread Basics - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!