Multithreading: thread, mutex, async

মাল্টিথ্রেডিং — thread, mutex, async

Read: ~40 min 14 practice problems

1. Spawning a Thread

thread.cpp
#include <iostream>
#include <thread>

void work(int id) {
    std::cout << "Hello from thread " << id << "\n";
}

int main() {
    std::thread t1(work, 1);
    std::thread t2([]() { std::cout << "lambda thread\n"; });
    t1.join();
    t2.join();
}
Always join or detach If a std::thread is destroyed while still joinable, std::terminate is called. C++20's std::jthread auto-joins.

2. Race Conditions

Two threads accessing the same data, at least one writing → undefined behavior. Protect shared data with a mutex.

race.cpp
#include <iostream>
#include <thread>
#include <mutex>

int counter = 0;
std::mutex m;

void inc(int n) {
    for (int i = 0; i < n; ++i) {
        std::lock_guard<std::mutex> lock(m);
        ++counter;
    }
}

int main() {
    std::thread a(inc, 100000);
    std::thread b(inc, 100000);
    a.join(); b.join();
    std::cout << counter << "\n"; // 200000
}
Always use lock_guard / scoped_lock Never call m.lock() / m.unlock() manually. RAII ensures unlock on exception.

3. std::async — Future Results

async.cpp
#include <iostream>
#include <future>

int slow(int x) { return x * x; }

int main() {
    std::future<int> f = std::async(std::launch::async, slow, 7);
    // ... do other work ...
    std::cout << f.get() << "\n"; // 49
}

4. Deadlocks

Thread A holds lock1, waits for lock2; thread B holds lock2, waits for lock1. Forever.

  • Always acquire locks in the same order across threads
  • Use std::scoped_lock(m1, m2) — atomically locks both
  • Hold locks for the shortest time possible

5. Atomic Variables

For simple counters, std::atomic is faster than mutex:

atomic.cpp
#include <atomic>

std::atomic<int> counter{0};
// Thread-safe ++counter, no mutex needed

6. Practice Problems

  1. Spawn 4 threads each printing their id.
    ✨ Show Answer
    std::vector<std::thread> ts;
    for (int i = 0; i < 4; ++i)
        ts.emplace_back([i]{ std::cout << i << "\n"; });
    for (auto& t : ts) t.join();
  2. Why use lock_guard instead of m.lock()?
    ✨ Show Answer

    RAII: unlock happens on scope exit even on exception. Manual lock/unlock leaks the lock if exceptions fly.

  3. Lock two mutexes safely.
    ✨ Show Answer
    std::scoped_lock lock(m1, m2);
  4. When is atomic better than mutex?
    ✨ Show Answer

    For single primitive types (int, pointer) where the operation is one read/write/RMW. Mutex has higher overhead and supports complex critical sections.

  5. async vs thread — when to use which?
    ✨ Show Answer

    Use std::async when you want a result. Use std::thread for fire-and-forget or long-running workers.

  6. What's the future returned by std::async?
    ✨ Show Answer

    A handle to the eventual result. Call .get() to wait for it; calling twice throws.

  7. Why does ~std::thread abort if joinable?
    ✨ Show Answer

    Forces you to be explicit about thread lifetime. Use std::jthread (C++20) to auto-join.

  8. Demonstrate a race condition.
    ✨ Show Answer
    int c = 0;
    auto work = []{ for(int i=0; i<100000; ++i) ++c; };
    std::thread a(work), b(work);
    a.join(); b.join();
    // c will likely NOT be 200000 — UB without sync
  9. Use a condition_variable for producer-consumer.
    ✨ Show Answer
    std::condition_variable cv;
    std::unique_lock<std::mutex> lock(m);
    cv.wait(lock, []{ return !queue.empty(); });
  10. Why does deadlock happen?
    ✨ Show Answer

    Two (or more) threads each holding a lock the other needs, with neither releasing. Solution: consistent lock order, or scoped_lock.

  11. Detach a thread.
    ✨ Show Answer
    t.detach(); // thread runs independently; can't join
  12. Why use ThreadSanitizer?
    ✨ Show Answer

    Detects data races at runtime. Compile with -fsanitize=thread.

  13. Pass a reference to a thread function.
    ✨ Show Answer
    std::thread t(func, std::ref(x));
  14. jthread benefit?
    ✨ Show Answer

    Auto-joins in destructor + supports cooperative cancellation via std::stop_token. C++20.

Summary

std::thread spawns a thread, join() waits, std::async returns a future. Protect shared mutable data with a mutex wrapped in std::lock_guard. Use std::atomic for simple counters. Test with -fsanitize=thread. Concurrency is hard — design for it from the start.

Next Module → File I/O & Streams (fstream).