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();
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