C++ jthread | "Auto-Join Threads" Guide
이 글의 핵심
A practical guide to C++ jthread.
Entering
std::jthread in C++20 is an improved thread class that provides automatic join and abort mechanism. It is safe and convenient by following the RAII principles.
1. jthread basics
std::thread vs std::jthread```cpp
#include
using namespace std::chrono_literals;
// std::thread: manual join required void useThread() { std::thread t( { std::cout << “std::thread operation” << std::endl; });
t.join(); // essential! If not, call std::terminate }
// std::jthread: auto join void useJthread() { std::jthread jt( { std::cout << “std::jthread operation” << std::endl; });
// Automatically joined in destructor }
int main() { useThread(); useJthread(); }
| Features | std::thread | std::jthread |
|------|-------------|-------------|
| auto join | ❌ (requires manual) | ✅ (in destructor) |
| suspension mechanism | ❌ | ✅ (stop_token) |
| RAII | ❌ | ✅ |
| C++ version | C++11 | C++20 |
Key Concepts:
- RAII: Automatically clean up resources in destructor.
- Safety: Prevent terminate due to missing join()
- Convenience: No explicit join required
---
## 2. Basic use
### Simple example```cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
void simpleTask() {
std::cout << "start operation" << std::endl;
std::this_thread::sleep_for(1s);
std::cout << "Operation completed" << std::endl;
}
int main() {
std::cout << "start main" << std::endl;
{
std::jthread t(simpleTask);
std::cout << "Thread created" << std::endl;
// Auto-join when out of scope
}
std::cout << "main end" << std::endl;
}
```### Passing parameters```cpp
#include <iostream>
#include <thread>
void printNumbers(int start, int end) {
for (int i = start; i <= end; i++) {
std::cout << i << " ";
}
std::cout << std::endl;
}
int main() {
std::jthread t1(printNumbers, 1, 5);
std::jthread t2(printNumbers, 10, 15);
// auto-joined
}
```---
## 3. Stopping with stop_token
### Default abort mechanism```cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
void worker(std::stop_token stoken) {
int count = 0;
while (!stoken.stop_requested()) {
std::cout << "Working... (" << count++ << ")" << std::endl;
std::this_thread::sleep_for(100ms);
}
std::cout << "aborted" << std::endl;
}
int main() {
std::jthread t(worker);
std::this_thread::sleep_for(1s);
std::cout << "Abort request" << std::endl;
t.request_stop(); // request to stop
// Automatic join (in destructor)
}
```### Utilizing stop_token```cpp
#include <iostream>
#include <thread>
#include <chrono>
#include <atomic>
using namespace std::chrono_literals;
void dataProcessor(std::stop_token stoken) {
std::atomic<int> processed{0};
while (!stoken.stop_requested()) {
// data processing
processed++;
// Check for interruption periodically
if (processed % 100 == 0) {
std::cout << "Processed data: " << processed << std::endl;
}
std::this_thread::sleep_for(10ms);
}
std::cout << "Final processed: " << processed << std::endl;
}
int main() {
std::jthread t(dataProcessor);
std::this_thread::sleep_for(2s);
t.request_stop();
}
```---
## 4. Practical example
### Example 1: RAII pattern```cpp
#include <iostream>
#include <thread>
#include <stdexcept>
#include <chrono>
using namespace std::chrono_literals;
void riskyOperation() {
std::jthread worker( {
for (int i = 0; i < 10; i++) {
std::cout << "task " << i << std::endl;
std::this_thread::sleep_for(100ms);
}
});
// Even if an exception occurs, workers are automatically joined.
if (rand() % 2 == 0) {
throw std::runtime_error("An exception occurred!");
}
std::cout << "normal end" << std::endl;
}
int main() {
try {
riskyOperation();
} catch (const std::exception& e) {
std::cout << "Exception handling: " << e.what() << std::endl;
}
std::cout << "main end" << std::endl;
}
```### Example 2: Managing multiple threads```cpp
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
using namespace std::chrono_literals;
void workerTask(int id, std::stop_token stoken) {
while (!stoken.stop_requested()) {
std::cout << "thread " << id << "working" << std::endl;
std::this_thread::sleep_for(200ms);
}
std::cout << "thread " << id << "end" << std::endl;
}
int main() {
std::vector<std::jthread> threads;
// Create 5 threads
for (int i = 0; i < 5; i++) {
threads.emplace_back(workerTask, i);
}
std::cout << "All threads running..." << std::endl;
std::this_thread::sleep_for(2s);
std::cout << "Request to stop all threads" << std::endl;
// Request all threads to stop
for (auto& t : threads) {
t.request_stop();
}
// Automatically join all threads when vector is destroyed
std::cout << "main end" << std::endl;
}
```### Example 3: Use with condition variables```cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
using namespace std::chrono_literals;
std::mutex mtx;
std::condition_variable_any cv;
std::queue<int> taskQueue;
void worker(std::stop_token stoken) {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// wait supporting stop_token
if (cv.wait(lock, stoken, []{ return !taskQueue.empty(); })) {
int task = taskQueue.front();
taskQueue.pop();
lock.unlock();
std::cout << "Process: " << task << std::endl;
std::this_thread::sleep_for(100ms);
}
if (stoken.stop_requested()) {
std::cout << "Exit worker" << std::endl;
break;
}
}
}
int main() {
std::jthread t(worker);
// Add task
for (int i = 0; i < 10; i++) {
{
std::lock_guard<std::mutex> lock(mtx);
taskQueue.push(i);
}
cv.notify_one();
std::this_thread::sleep_for(50ms);
}
std::this_thread::sleep_for(2s);
t.request_stop();
cv.notify_one(); // Wake up a waiting thread
}
```---
## 5. Advanced use of stop_token
### stop_callback```cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
void worker(std::stop_token stoken) {
// Register callback when requesting abort
std::stop_callback callback(stoken, {
std::cout << "Abort callback called!" << std::endl;
});
int count = 0;
while (!stoken.stop_requested()) {
std::cout << "task " << count++ << std::endl;
std::this_thread::sleep_for(200ms);
}
}
int main() {
std::jthread t(worker);
std::this_thread::sleep_for(1s);
t.request_stop(); // callback is called immediately
}
stop_source
#include <iostream>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
void worker(std::stop_token stoken) {
while (!stoken.stop_requested()) {
std::cout << "Working..." << std::endl;
std::this_thread::sleep_for(200ms);
}
}
int main() {
std::stop_source ssource;
std::stop_token stoken = ssource.get_token();
std::jthread t(worker, stoken);
std::this_thread::sleep_for(1s);
ssource.request_stop(); // Request to stop with stop_source
}
```---
## 6. Frequently occurring problems
### Issue 1: Missing join (std::thread)```cpp
#include <thread>
#include <iostream>
// ❌ std::thread: terminate when join is missing
void badExample() {
std::thread t( {
std::cout << "task" << std::endl;
});
// Destroyed without join() or detach()
// → call std::terminate!
}
// ✅ std::thread: explicit join
void goodExample1() {
std::thread t( {
std::cout << "task" << std::endl;
});
t.join(); // essential
}
// ✅ std::jthread: auto join
void goodExample2() {
std::jthread t( {
std::cout << "task" << std::endl;
});
// Automatic join in destructor
}
```### Issue 2: Missing break check```cpp
#include <thread>
#include <iostream>
#include <chrono>
using namespace std::chrono_literals;
// ❌ No interruption check (infinite loop)
void badWorker(std::stop_token stoken) {
while (true) {
std::cout << "Working..." << std::endl;
std::this_thread::sleep_for(100ms);
// no stop_requested() check!
}
}
// ✅ Periodically check for interruptions
void goodWorker(std::stop_token stoken) {
while (!stoken.stop_requested()) {
std::cout << "Working..." << std::endl;
std::this_thread::sleep_for(100ms);
}
std::cout << "normal end" << std::endl;
}
```### Problem 3: Using detach```cpp
#include <thread>
#include <iostream>
int main() {
std::jthread t( {
std::cout << "task" << std::endl;
});
// ❌ No automatic join after detachment
t.detach();
// Thread runs in background
// When main terminates, threads may also be forcibly terminated.
}
```### Issue 4: Move Semantics```cpp
#include <thread>
#include <iostream>
int main() {
std::jthread t1( {
std::cout << "task" << std::endl;
});
// ✅ Moveable
std::jthread t2 = std::move(t1);
// t1 is no longer valid
// t1.request_stop(); // undefined behavior
// t2 is valid
t2.request_stop();
}
```---
## 7. Practical example: Background task manager```cpp
#include <iostream>
#include <thread>
#include <vector>
#include <functional>
#include <chrono>
using namespace std::chrono_literals;
class TaskManager {
public:
using Task = std::function<void(std::stop_token)>;
void addTask(Task task) {
threads.emplace_back(task);
}
void stopAll() {
std::cout << "Request to stop all operations" << std::endl;
for (auto& t : threads) {
t.request_stop();
}
}
size_t activeCount() const {
return threads.size();
}
~TaskManager() {
std::cout << "TaskManager destruction (auto join)" << std::endl;
}
private:
std::vector<std::jthread> threads;
};
int main() {
TaskManager manager;
// Task 1: Counter
manager.addTask( {
int count = 0;
while (!stoken.stop_requested()) {
std::cout << "Counter: " << count++ << std::endl;
std::this_thread::sleep_for(300ms);
}
});
//Task 2: Monitor
manager.addTask( {
while (!stoken.stop_requested()) {
std::cout << "Monitoring..." << std::endl;
std::this_thread::sleep_for(500ms);
}
});
// Task 3: Logger
manager.addTask( {
while (!stoken.stop_requested()) {
std::cout << "Log record" << std::endl;
std::this_thread::sleep_for(1s);
}
});
std::cout << "Active task: " << manager.activeCount() << "count" << std::endl;
std::this_thread::sleep_for(3s);
manager.stopAll();
std::this_thread::sleep_for(1s);
std::cout << "main end" << std::endl;
}
```---
## Cleanup
### Key takeaways
1. **Auto Join**: Automatically join() in destructor
2. **stop_token**: Cooperative stop mechanism
3. **RAII**: Exception safety guarantee
4. **stop_callback**: Execute callback when stopped
5. **std::thread replacement**: jthread recommended in C++20
### std::thread vs std::jthread
| Features | std::thread | std::jthread |
|------|-------------|-------------|
| join | manual (`join()`) | auto (destructor) |
| interruption | None | `stop_token` |
| exception safety | low | High (RAII) |
| Ease of use | Normal | High |
| C++ version | C++11 | C++20 |
### Practical tips
1. **Suspend Mechanism**
- Check `stop_requested()` periodically
- Add checkpoints in the middle of long tasks
- Perform cleanup operations with `stop_callback`
2. **Performance**
- Performance of jthread and thread is the same
- Automatic join makes code concise
- Reduce bugs with exception safety
3. **Migration**
- Easily switch from `std::thread` → `std::jthread`
- Stop support by adding stop_token parameter
- Remove existing join() call
### Next steps
- [C++ stop_token](/blog/cpp-stop-token/)
- [C++ thread_local](/blog/cpp-thread-local/)
- [C++ Multithreading Basics](/blog/cpp-multithreading-basics/)
---
## Related articles
- [C++ stop_token | ](/blog/cpp-stop-token/)
- [C++ Barrier & Latch | ](/blog/cpp-barrier-latch/)
- [C++ Branch Prediction | ](/blog/cpp-branch-prediction/)
- [C++ Calendar & Timezone | ](/blog/cpp-calendar-timezone/)
- [Modern C++ (C++11~C++20) Core Grammar Cheat Sheet | A glance at frequently used items in the workplace](/blog/cpp-cheatsheet-02-modern-cpp-syntax/)