Multithreading in C++

Multithreading lets your program do more than one thing at the same time. Each independent task runs in a thread — a unit of execution managed by the operating system. Multiple threads inside the same program share memory and run concurrently, making programs faster on multi-core processors.

Think of threads like workers in a kitchen. A single cook (one thread) prepares everything one step at a time. Multiple cooks (multiple threads) work on different dishes simultaneously, finishing the meal much faster. C++11 introduced the <thread> library to support multithreading directly.

Single Thread vs Multi-Thread

Single-threaded:
Task A → Task B → Task C (sequential, takes longer)

Multi-threaded:
Thread 1: Task A ─────────────────────→ done
Thread 2: Task B ─────────────→ done
Thread 3: Task C ──────────────────────────→ done
All run at the same time — total time = longest task

Creating a Thread

Include <thread> and create a std::thread object, passing it the function the thread should run.

#include <iostream>
#include <thread>
using namespace std;

void greet(string name) {
    cout << "Hello from thread: " << name << endl;
}

int main() {
    thread t1(greet, "Alice");
    thread t2(greet, "Bob");

    t1.join();   // wait for t1 to finish
    t2.join();   // wait for t2 to finish

    cout << "Both threads completed." << endl;
    return 0;
}

Sample Output (order may vary):

Hello from thread: Alice
Hello from thread: Bob
Both threads completed.

The order of "Alice" and "Bob" may differ each run because both threads run concurrently and the OS decides which one gets CPU time first.

join() and detach()

After creating a thread, you must decide what to do with it before the program ends.

MethodMeaning
t.join()Main thread waits here until thread t finishes
t.detach()Thread t runs independently; main does not wait for it
t.joinable()Returns true if the thread can still be joined or detached

If you neither join nor detach a thread before the program exits, the program terminates abnormally (std::terminate is called).

Threads with Lambdas

#include <iostream>
#include <thread>
using namespace std;

int main() {
    thread t([]() {
        for (int i = 1; i <= 5; i++) {
            cout << "Worker: " << i << endl;
        }
    });

    for (int i = 1; i <= 5; i++) {
        cout << "Main:   " << i << endl;
    }

    t.join();
    return 0;
}

Sample Output (interleaved, order varies):

Main:   1
Worker: 1
Main:   2
Worker: 2
...

The Race Condition Problem

When multiple threads read and write the same variable at the same time without coordination, the result becomes unpredictable. This is called a race condition.

#include <iostream>
#include <thread>
using namespace std;

int counter = 0;

void increment() {
    for (int i = 0; i < 100000; i++) {
        counter++;   // NOT thread-safe!
    }
}

int main() {
    thread t1(increment);
    thread t2(increment);

    t1.join();
    t2.join();

    cout << "Counter: " << counter << endl;  // expected 200000, but result varies
    return 0;
}

Sample Output:

Counter: 174382   (wrong! race condition)

Why it happens:

Thread 1 reads counter = 50
Thread 2 reads counter = 50   ← both read the same value
Thread 1 writes counter = 51
Thread 2 writes counter = 51  ← one increment is lost!

Fixing Race Conditions with mutex

A mutex (mutual exclusion) is a lock. Only one thread can hold the lock at a time. Other threads wait until the lock is released. This ensures that only one thread modifies shared data at a time.

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int counter = 0;
mutex mtx;

void safeIncrement() {
    for (int i = 0; i < 100000; i++) {
        mtx.lock();      // acquire the lock
        counter++;       // only one thread runs this at a time
        mtx.unlock();    // release the lock
    }
}

int main() {
    thread t1(safeIncrement);
    thread t2(safeIncrement);

    t1.join();
    t2.join();

    cout << "Counter: " << counter << endl;  // always 200000
    return 0;
}

Output:

Counter: 200000

lock_guard — Safer Mutex Handling

Calling lock() and unlock() manually is risky. If the code throws an exception between them, unlock() never runs and the program deadlocks. lock_guard locks the mutex automatically and unlocks it when it goes out of scope — even if an exception occurs.

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int counter = 0;
mutex mtx;

void safeIncrement() {
    for (int i = 0; i < 100000; i++) {
        lock_guard<mutex> guard(mtx);   // auto-unlock when guard goes out of scope
        counter++;
    }
}

int main() {
    thread t1(safeIncrement);
    thread t2(safeIncrement);

    t1.join();
    t2.join();

    cout << "Counter: " << counter << endl;
    return 0;
}

Thread with Return Value — std::future and std::async

std::async runs a function asynchronously and returns a std::future object. You call .get() on the future to retrieve the result when the thread finishes.

#include <iostream>
#include <future>
using namespace std;

int computeSum(int a, int b) {
    return a + b;
}

int main() {
    future<int> result = async(computeSum, 30, 70);
    cout << "Sum: " << result.get() << endl;  // waits for result
    return 0;
}

Output:

Sum: 100

Common Multithreading Issues

┌──────────────────┬─────────────────────────────────────────────────────┐
│ Problem          │ Description                                         │
├──────────────────┼─────────────────────────────────────────────────────┤
│ Race condition   │ Two threads modify shared data simultaneously       │
│ Deadlock         │ Two threads each wait for a lock the other holds    │
│ Starvation       │ A thread never gets a chance to run                 │
│ Data corruption  │ Unsynchronized writes leave data in invalid state   │
└──────────────────┴─────────────────────────────────────────────────────┘

Key Takeaways

  • Threads allow concurrent execution of code within the same program.
  • Create threads with std::thread; always call join() or detach() before the thread object is destroyed.
  • Race conditions occur when multiple threads access shared data without synchronization.
  • Use std::mutex to protect shared data; prefer lock_guard over manual lock/unlock.
  • Use std::async and std::future when a thread needs to return a value.
  • Compile multithreading code with the -lpthread flag on Linux/GCC.

Leave a Comment

Your email address will not be published. Required fields are marked *