Thread IDs and Hardware Concurrency
Learn about thread identification, hardware capabilities, and thread-local storage.
What Are Thread IDs and Hardware Concurrency?
Thread IDs are unique identifiers assigned to each running thread, useful for debugging and logging. Hardware concurrency tells you how many threads the system can run truly in parallel, which helps you decide how many threads to create.
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
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
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!