Concurrency and Threads
Concurrency describes the simultaneous execution of several independent activities within one system. On a single-core CPU, only one instruction stream can run at any instant, yet the operating system rapidly switches among tasks, giving the illusion of parallelism. Each switch incurs a context-switch cost: the current state, registers, and instruction pointer must be saved, the scheduler chooses the next runnable task, and the processor reloads the new context. These gray slices of overhead make concurrent execution slower than running tasks back-to-back on a single core.
True parallelism—hardware concurrency—arrives with multi-core CPUs, where each core can execute a separate insrtuction stream simultaneously.
Two common concurrency models exist:
- Multi-process concurrency – Independent processes communicate through pipes, message queues, shared memory, semaphores, memory-mapped I/O, or sockets. IPC is heavyweight, slower, and resource-intensive.
- Multi-thread concurrency – Threads are lightweight execution units that share the same virtual address space. Communication is often as simple as reading or writing global variables.
The C++11 Thread Facility
C++11 introduced <thread> and related headers, giving portable primitives for thread creation, synchronization, and atomic operations.
Spawning Threads
A std::thread object starts immediately upon construction. The caller must decide, before the object is destroyed, whether to join (block until completion) or detach (let the runtime reap resources).
Free and Static Functions
static void counterTask() {
for (int i = 0; i <= 10; ++i) {
std::cout << "counterTask id=" << std::this_thread::get_id()
<< " value=" << i << '\n';
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "counterTask exiting\n";
}
void cpuReporter() {
while (true) {
std::cout << "cpuReporter id=" << std::this_thread::get_id()
<< " cores=" << std::thread::hardware_concurrency() << '\n';
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main() {
std::thread t1(counterTask); // runs counterTask
t1.join(); // wait for completion
std::thread t2(cpuReporter);
std::cout << "native handle: " << t2.native_handle() << '\n';
t2.join();
}
Passing Arguments
Use std::ref to forward references safely:
void echoValue(int& v) {
while (true) {
std::cout << "echoValue sees " << v << " from thread "
<< std::this_thread::get_id() << '\n';
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main() {
int value = 42;
std::thread worker(echoValue, std::ref(value));
worker.join(); // OK: value outlives thread
}
Detached threads must not access automatic variables that may vanish.
Sleeping Strategies
sleep_for pauses for a relative druation; sleep_until wakes at an absolute time point.
void steadyHeartbeat() {
using clock = std::chrono::steady_clock;
auto nextTick = clock::now();
while (true) {
nextTick += std::chrono::milliseconds(1000);
std::this_thread::sleep_until(nextTick);
std::cout << "tick from " << std::this_thread::get_id() << '\n';
}
}
Functor Objects
Any object with an operator() can act as a thread target:
class Worker {
public:
explicit Worker(std::string name) : name_(std::move(name)) {}
void operator()() const {
while (true) {
std::cout << name_ << " running\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
private:
std::string name_;
};
int main() {
Worker w("printer");
std::thread job(w);
job.join();
}
Member Functions
A pointer-to-member plus an object instance launches a member function in a new thread:
class Engine {
public:
void spin() {
while (true) {
std::cout << "Engine spinning\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
};
int main() {
Engine e;
std::thread t(&Engine::spin, &e); // pass object pointer
t.detach(); // runs independently
}
Lambda Expressions
Lambdas capture surrounding variables and yield concise inline threads:
int main() {
int shared = 123;
std::thread lambdaThread([&] {
while (true) {
std::cout << "lambda sees " << shared
<< " in thread " << std::this_thread::get_id() << '\n';
std::this_thread::sleep_for(std::chrono::seconds(1));
}
});
lambdaThread.join();
}
Thread lifetime, ownership, and variable capture remain the programmer’s responsibility; misuse leads to data races or dangling references.