Multithreading: thread, mutex, async
মাল্টিথ্রেডিং — thread, mutex, async
1. Spawning a Thread
#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();
}
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.
#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
}
m.lock() / m.unlock() manually. RAII ensures unlock on exception.
3. std::async — Future Results
#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:
#include <atomic>
std::atomic<int> counter{0};
// Thread-safe ++counter, no mutex needed
6. Practice Problems
- 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(); - 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.
- Lock two mutexes safely.
✨ Show Answer
std::scoped_lock lock(m1, m2); - 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.
- async vs thread — when to use which?
✨ Show Answer
Use
std::asyncwhen you want a result. Usestd::threadfor fire-and-forget or long-running workers. - 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. - Why does
~std::threadabort if joinable?✨ Show Answer
Forces you to be explicit about thread lifetime. Use
std::jthread(C++20) to auto-join. - 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 - 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(); }); - 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.
- Detach a thread.
✨ Show Answer
t.detach(); // thread runs independently; can't join - Why use ThreadSanitizer?
✨ Show Answer
Detects data races at runtime. Compile with
-fsanitize=thread. - Pass a reference to a thread function.
✨ Show Answer
std::thread t(func, std::ref(x)); - 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.