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();
After a thread is joined or detached, its ID becomes `std::thread::id()` (a default-constructed, "null" ID).
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.

The function may return 0 if it cannot determine the value. Always handle this case:
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
    }
}
`yield()` is just a hint - the scheduler may ignore it. It is most useful in busy-wait loops to avoid burning CPU unnecessarily.

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 ID
  • std::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() and sleep_until() pause the current thread
  • yield() voluntarily gives up the current time slice
  • thread_local creates per-thread instances of variables