Basic Thread Management in C++
Master basic C++ thread management! Learn to launch threads with functions, functors, and lambdas, pass arguments, and choose between join() vs. detach().
In the previous lesson, you learned how to create a simple thread using std::thread and join().
Now, let’s explore a few more ways to launch threads and manage their lifetime — using functions, functors, and lambdas — plus how to wait or let them run independently.
1. Launching a thread with a function
The simplest way to start a thread is to pass a function to the std::thread constructor.
#include <iostream>
#include <thread>
void work() {
std::cout << "Working in thread\n";
}
int main() {
std::thread t(work);
t.join(); // Wait for the thread to finish
}
Output:
Working in thread
Explanation:
The new thread begins running work().
Calling join() ensures the main thread waits until it finishes.
2. Launching a thread with a callable object (functor)
Any object that overloads the function call operator operator() can also be used as a thread entry point.
This is powerful because a functor can store state (configuration, counters, etc.) and reuse it across calls.
#include <iostream>
#include <thread>
struct Counter {
int count = 0;
void operator()() {
++count;
std::cout << "Running task #" << count << "\n";
}
};
int main() {
Counter worker;
// Each thread gets its own copy
std::thread t1(worker);
std::thread t2(worker);
t1.join();
t2.join();
std::cout << "Final count in main: " << worker.count << "\n";
}
Output:
Running task #1
Running task #1
Final count in main: 0
Each thread receives a copy of the worker, so the changes happen on the copies.
To share the same object between threads, use std::ref():
#include <iostream>
#include <thread>
struct Counter {
int count = 0;
void operator()() {
++count;
std::cout << "Running task #" << count << "\n";
}
};
int main() {
Counter worker;
std::thread t1(std::ref(worker));
std::thread t2(std::ref(worker));
t1.join();
t2.join();
std::cout << "Final count in main: " << worker.count << "\n";
}
Output:
Running task #Running task #2
2
Final count in main: 2
Warning: There might be a data race here in this simple example, since one count is shared across threads t1 and t2.
Why Output looks strange
-
Both threads share the same
std::coutstream, and its output is not synchronized between threads by default. -
When multiple threads write to
std::coutat the same time, their text can interleave — producing partial or jumbled lines. -
The thread scheduler decides when each thread runs. One thread might start printing but get interrupted before finishing a line, letting the other continue immediately.
-
++countis performed rightt1andt2are created, socountmight be already 2 before it is passed tostd::cout.
Why functors matter
They’re useful when:
- You want to keep per-thread state (like configuration or counters).
- You prefer organizing thread logic inside a class instead of global functions.
- You want to reuse the same callable object across multiple threads.
3. Launching a thread with a lambda expression
Lambdas make threading even simpler, especially for short, inline tasks.
#include <iostream>
#include <thread>
int main() {
std::thread t([] {
std::cout << "Working in lambda thread\n";
});
t.join();
}
Output:
Working in lambda thread
This is ideal when the code is small and only used once.
4. Passing arguments to threads
Arguments are copied by default when passed to a thread.
#include <iostream>
#include <thread>
void greet(std::string name) {
std::cout << "Hello, " << name << "\n";
}
int main() {
std::thread t(greet, "C++ threads");
t.join();
}
Output:
Hello, C++ threads
To pass by reference, use std::ref():
#include <iostream>
#include <thread>
void increment(int& x) {
++x;
}
int main() {
int value = 0;
std::thread t(increment, std::ref(value));
t.join();
std::cout << "Value = " << value << "\n";
}
Output:
Value = 1
5. join() vs detach()
join() waits for the thread to finish before continuing — this is synchronous and safe.
std::thread t(work);
t.join(); // Waits for work() to finish
detach() lets the thread run independently in the background.
The main thread does not wait for it.
#include <iostream>
#include <thread>
void background() {
std::cout << "Background task done\n";
}
int main() {
std::thread t(background);
t.detach(); // Run in background
std::cout << "Main is done\n";
}
If the main thread runs too fast, you might see the output only Main is done when the program terminates. The t thread continues running even after t goes out of scope. Its ownership and control are passed over to the C++ Runtime Library.
Use case: logging in the background
Detached threads are useful for background tasks such as logging.
#include <iostream>
#include <thread>
#include <chrono>
void logMessage(std::string msg) {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "[LOG] " << msg << "\n";
}
int main() {
std::thread logger(logMessage, "Application started");
logger.detach(); // runs independently
std::cout << "Main continues working...\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
}
Output:
Main continues working...
[LOG] Application started
Warning
- Detached threads are useful, but dangerous if they access data owned by the main thread.
- Make sure they operate only on data that remains valid while they run.
Common pitfall: forgetting join() or detach()
If neither is called before the thread object is destroyed, the program terminates with:
terminate called without an active exception
Always ensure every thread is either joined or detached.
Takeaway
From this lesson, you learned:
- Three ways to launch threads — with a function, functor, or lambda.
- How to pass arguments (by value or by reference using
std::ref()). - The purpose and behavior of
join()anddetach(). - Why functors are useful for maintaining state across calls.
- What happens with a detached thread’s object and when to use it safely.
You now understand the basics of thread management — how to launch, synchronize, and safely clean up threads in a C++ program.