Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Thread IDs and Hardware Concurrency
Learn about thread identification, hardware capabilities, and thread-local storage.
Thread IDs and Hardware Concurrency
Every thread has a unique identifier - its thread ID. The C++ standard library provides std::thread::id to represent thread identifiers.
Getting Thread IDs
Getting the Current Thread's ID
Use std::this_thread::get_id() to get the ID of the currently executing thread:
#include <iostream>
#include <thread>
void print_id() {
std::cout << "Thread ID: " << std::this_thread::get_id() << '\n';
}
int main() {
std::cout << "Main thread ID: " << std::this_thread::get_id() << '\n';
std::thread t(print_id);
t.join();
return 0;
}
Output (IDs will vary):
Main thread ID: 140735842446144
Thread ID: 140735833890560
Getting a Thread's ID from std::thread
Use the get_id() member function on a std::thread object:
std::thread t(work);
std::cout << "Thread t has ID: " << t.get_id() << '\n';
t.join();
std::thread t(work);
std::cout << "Before join: " << t.get_id() << '\n'; // Valid ID
t.join();
std::cout << "After join: " << t.get_id() << '\n'; // Null ID
Properties of Thread IDs
Comparison
Thread IDs can be compared for equality:
std::thread::id main_id = std::this_thread::get_id();
void check_main_thread() {
if (std::this_thread::get_id() == main_id) {
std::cout << "Running on main thread\n";
} else {
std::cout << "Running on a different thread\n";
}
}
int main() {
check_main_thread(); // Running on main thread
std::thread t(check_main_thread); // Running on a different thread
t.join();
}
Ordering
Thread IDs support all comparison operators (<, >, <=, >=), so they can be used in ordered containers:
#include <map>
#include <thread>
std::map<std::thread::id, std::string> thread_names;
void register_thread(const std::string& name) {
thread_names[std::this_thread::get_id()] = name;
}
Hashing
Thread IDs can be hashed, making them usable in unordered containers:
#include <unordered_map>
#include <thread>
std::unordered_map<std::thread::id, int> thread_data;
void store_data(int value) {
thread_data[std::this_thread::get_id()] = value;
}
The Null ID
A default-constructed std::thread::id represents "no thread":
std::thread::id null_id; // Default constructed
std::thread t; // Not associated with any thread
std::cout << std::boolalpha;
std::cout << (t.get_id() == null_id) << '\n'; // true
t = std::thread(work);
std::cout << (t.get_id() == null_id) << '\n'; // false
t.join();
std::cout << (t.get_id() == null_id) << '\n'; // true again
Practical Uses of Thread IDs
Thread-Local Logging
#include <iostream>
#include <thread>
#include <sstream>
#include <mutex>
std::mutex cout_mutex;
void log(const std::string& message) {
std::lock_guard<std::mutex> lock(cout_mutex);
std::cout << "[Thread " << std::this_thread::get_id() << "] "
<< message << '\n';
}
void worker(int id) {
log("Starting work");
// ... do work ...
log("Finished work");
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join();
t2.join();
}
Per-Thread Data Tracking
#include <map>
#include <mutex>
class ThreadStats {
std::map<std::thread::id, size_t> operations_;
std::mutex mutex_;
public:
void record_operation() {
std::lock_guard<std::mutex> lock(mutex_);
++operations_[std::this_thread::get_id()];
}
void print_stats() {
std::lock_guard<std::mutex> lock(mutex_);
for (const auto& [id, count] : operations_) {
std::cout << "Thread " << id << ": " << count << " operations\n";
}
}
};
Single-Thread Enforcement
Sometimes you want to ensure certain code only runs on a specific thread:
class MainThreadOnly {
std::thread::id main_thread_id_;
public:
MainThreadOnly() : main_thread_id_(std::this_thread::get_id()) {}
void must_run_on_main_thread() {
if (std::this_thread::get_id() != main_thread_id_) {
throw std::runtime_error("Must be called from main thread!");
}
// ... do main-thread-only work ...
}
};
Hardware Concurrency
How many threads should you create? Creating more threads than your CPU can run in parallel often hurts performance due to context-switching overhead.
Querying Available Cores
std::thread::hardware_concurrency() returns a hint about the number of hardware threads available:
#include <iostream>
#include <thread>
int main() {
unsigned int cores = std::thread::hardware_concurrency();
std::cout << "Hardware concurrency: " << cores << '\n';
}
This typically returns the number of logical processors (including hyperthreaded cores). On a 4-core CPU with hyperthreading, it might return 8.
unsigned int num_threads = std::thread::hardware_concurrency();
if (num_threads == 0) {
num_threads = 2; // Reasonable default
}
Using Hardware Concurrency for Thread Pools
#include <vector>
#include <thread>
void process_chunk(const std::vector<int>& data, size_t start, size_t end) {
for (size_t i = start; i < end; ++i) {
// Process data[i]...
}
}
void parallel_process(std::vector<int>& data) {
unsigned int num_threads = std::thread::hardware_concurrency();
if (num_threads == 0) num_threads = 2;
std::vector<std::thread> threads;
size_t chunk_size = data.size() / num_threads;
for (unsigned int i = 0; i < num_threads; ++i) {
size_t start = i * chunk_size;
size_t end = (i == num_threads - 1) ? data.size() : start + chunk_size;
threads.emplace_back(process_chunk, std::cref(data), start, end);
}
for (auto& t : threads) {
t.join();
}
}
Thread Count Considerations
| Workload Type | Recommended Thread Count |
|---|---|
| CPU-bound | hardware_concurrency() or slightly less |
| I/O-bound | Can be much higher (threads wait for I/O) |
| Mixed | Experiment to find optimal value |
For CPU-bound work:
// Leave one core for system/UI
unsigned int workers = std::max(1u, std::thread::hardware_concurrency() - 1);
For I/O-bound work:
// I/O-bound tasks spend most time waiting
// Can have many more threads than cores
unsigned int workers = std::thread::hardware_concurrency() * 4;
Sleeping and Yielding
std::this_thread provides functions for controlling the current thread's execution.
sleep_for - Sleep for a Duration
#include <chrono>
#include <thread>
void work() {
std::cout << "Starting...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "Done!\n";
}
Common duration types:
using namespace std::chrono_literals; // C++14
std::this_thread::sleep_for(100ms); // Milliseconds
std::this_thread::sleep_for(2s); // Seconds
std::this_thread::sleep_for(500us); // Microseconds
sleep_until - Sleep Until a Time Point
#include <chrono>
#include <thread>
void work() {
auto wake_time = std::chrono::steady_clock::now() + std::chrono::seconds(1);
// Do some work...
std::this_thread::sleep_until(wake_time); // Sleep for remaining time
}
This is useful when you want a consistent interval regardless of how long work takes.
yield - Give Up Time Slice
yield() hints to the scheduler that the current thread is willing to give up its remaining time slice:
void spin_wait(std::atomic<bool>& flag) {
while (!flag.load()) {
std::this_thread::yield(); // Let other threads run
}
}
Thread-Local Storage
While not strictly part of thread identification, thread-local storage is related - it is data that exists separately for each thread.
thread_local Keyword
thread_local int counter = 0; // Each thread has its own copy
void increment() {
++counter; // Modifies this thread's copy only
std::cout << "Thread " << std::this_thread::get_id()
<< " counter: " << counter << '\n';
}
int main() {
std::thread t1([]() { for(int i = 0; i < 5; ++i) increment(); });
std::thread t2([]() { for(int i = 0; i < 5; ++i) increment(); });
t1.join();
t2.join();
}
Each thread's counter increments independently from 1 to 5.
Use Cases for Thread-Local Storage
- Per-thread caches
- Per-thread random number generators
- Error codes (like
errno) - Per-thread statistics
thread_local std::mt19937 rng(std::random_device{}());
int random_int(int min, int max) {
// Each thread has its own generator - no synchronization needed
std::uniform_int_distribution<int> dist(min, max);
return dist(rng);
}
Summary
std::this_thread::get_id()returns the current thread's IDstd::thread::get_id()returns the ID of the thread represented by the object- Thread IDs can be compared, ordered, and hashed
std::thread::hardware_concurrency()returns the number of available hardware threads (or 0)- Match thread count to workload type: CPU-bound is approximately the core count, I/O-bound can be higher
sleep_for()andsleep_until()pause the current threadyield()voluntarily gives up the current time slicethread_localcreates per-thread instances of variables
Thread IDs and Hardware Concurrency - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!