C++: Multithreading Synchronization

Last updated: 2026-08-26

In lesson 43, we learned the basics of multithreading.

Now, we'll dive into multithreading synchronization — making multiple threads coordinate their work and avoiding races and deadlocks.

The challenge of multithreaded programming isn't creating threads — it's synchronization.


1. Deadlock

(1) 1.1 What Is a Deadlock?

A deadlock occurs when two or more threads wait for each other to release resources, so neither can continue.

Four necessary conditions:

  1. Mutual exclusion: Resources cannot be shared
  2. Hold and wait: A thread holding resources waits for other resources
  3. No preemption: Resources cannot be forcibly released
  4. Circular wait: A circular chain of waiting threads exists

(2) 1.2 Deadlock Example

Example: Deadlock (Difficulty ⭐⭐⭐)

▶ Example 2: Multithreading programming demo (Difficulty ⭐)

TEXT 📖 Display only
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx1, mtx2;

void thread1() {
 std::lock_guard<std::mutex> lock1(mtx1);
 std::this_thread::sleep_for(std::chrono::milliseconds(100));
 std::lock_guard<std::mutex> lock2(mtx2); // Waiting for mtx2
 std::cout << "Thread 1 done" << std::endl;
}

void thread2() {
 std::lock_guardstd::mutex lock2(mtx2);
 std::this_thread::sleep_for(std::chrono::milliseconds(100));
 std::lock_guardstd::mutex lock1(mtx1); // Waiting for mtx1
 std::cout << "Thread 2 done" << std::endl;
}

int main() {
 std::thread t1(thread1);
 std::thread t2(thread2);
 
 t1.join();
 t2.join();
 
 return 0;
}

Output:

TEXT 📖 Display only
Thread 1 done
Thread 2 done

💡 Tip:


(3) 1.3 Avoiding Deadlocks

Method 1: Fixed Locking Order

CPP
void thread1() {
 std::lock_guardstd::mutex lock1(mtx1);
 std::lock_guardstd::mutex lock2(mtx2); // Always mtx1 first, then mtx2
 std::cout << "Thread 1 done" << std::endl;
}

void thread2() {
 std::lock_guardstd::mutex lock1(mtx1); // Same order as thread1
 std::lock_guardstd::mutex lock2(mtx2);
 std::cout << "Thread 2 done" << std::endl;
}

Method 2: Use std::lock to lock multiple mutexes simultaneously

TEXT 📖 Display only
void safe_function() {
 std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
 std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
 std::lock(lock1, lock2); // Lock simultaneously, avoids deadlock
 
 // ...
}

Method 3: Use std::scoped_lock (C++17, recommended)

CPP
void safe_function() {
 std::scoped_lock lock(mtx1, mtx2); // Automatically avoids deadlock
 
 // ...
}


2. Read-Write Lock

(1) 2.1 Why Do We Need Read-Write Locks?

Problem: Mutexes are too "conservative" — even when multiple threads only read data, they must queue up.

Solution: Read-write lock (std::shared_mutex, C++17)

Lock Type Purpose
Exclusive lock Write operation, other threads cannot read or write
Shared lock Read operation, multiple threads can read simultaneously

(2) 2.2 Example: Protecting a cache with read-write lock (Difficulty ⭐⭐⭐)

CPP
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <unordered_map>

std::unordered_map<int, int> cache;
std::shared_mutex smtx;

int get(int key) {
 std::shared_lock<std::shared_mutex> lock(smtx); // Shared lock (read)
 auto it = cache.find(key);
 if (it != cache.end()) {
 return it->second;
 }
 return -1;
}

void set(int key, int value) {
 std::unique_lock<std::shared_mutex> lock(smtx); // Exclusive lock (write)
 cache[key] = value;
}

int main() {
 // Multiple threads can read simultaneously; writes are exclusive
 return 0;
}

Output:

TEXT 📖 Display only
(Program output)


3. Atomic Operations

(1) 3.1 What Is an Atomic Operation?

An atomic operation is an indivisible operation — it either executes completely or not at all.

Advantage: No locking needed, high performance.


(2) 3.2 std::atomic

C++11 provides the std::atomic template.

Example: Atomic counter (Difficulty ⭐⭐)

CPP
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>

std::atomic<int> counter(0); // Atomic variable

void increment() {
 for (int i = 0; i < 1000; i++) {
 counter++; // Atomic operation, no lock needed
 }
}

int main() {
 std::vector<std::thread> threads;
 for (int i = 0; i < 10; i++) {
 threads.emplace_back(increment);
 }
 
 for (auto& t : threads) {
 t.join();
 }
 
 std::cout << "Counter: " << counter << std::endl; // Always 10000
 return 0;
}

(3) 3.3 Memory Order

std::atomic supports memory order to control the scope of synchronization.

Common memory orders:



4. Thread Pool

(1) 4.1 Why Do We Need a Thread Pool?

Problem: Frequent thread creation/destruction has high overhead.

Solution: Thread pool — pre-create a set of threads and reuse them.


(2) 4.2 Simple Thread Pool Implementation

Example: Thread pool (Difficulty ⭐⭐⭐⭐)

TEXT 📖 Display only
#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>

class ThreadPool {
private:
 std::vector<std::thread> workers;
 std::queue<std::function<void()>> tasks;
 std::mutex mtx;
 std::condition_variable cv;
 bool stop;

public:
 ThreadPool(size_t numThreads) : stop(false) {
 for (size_t i = 0; i < numThreads; i++) {
 workers.emplace_back([this] {
 while (true) {
 std::function<void()> task;
 {
 std::unique_lock<std::mutex> lock(mtx);
 cv.wait(lock, [this] {
 return stop || !tasks.empty();
 });
 
 if (stop && tasks.empty()) {
 return;
 }
 
 task = std::move(tasks.front());
 tasks.pop();
 }
 task();
 }
 });
 }
 }
 
 ~ThreadPool() {
 {
 std::lock_guard<std::mutex> lock(mtx);
 stop = true;
 }
 cv.notify_all();
 for (auto& t : workers) {
 t.join();
 }
 }
 
 template<class F>
 void enqueue(F&& f) {
 {
 std::lock_guardstd::mutex lock(mtx);
 tasks.emplace(std::forward<F>(f));
 }
 cv.notify_one();
 }
};

int main() {
 ThreadPool pool(4); // Create 4 threads
 
 for (int i = 0; i < 10; i++) {
 pool.enqueue([i] {
 std::cout << "Task " << i << " running" << std::endl;
 });
 }
 
 return 0;
}


5. Practice: Parallel Sorting

▶ Example 1: Accelerating sorting with multithreading (Difficulty ⭐⭐⭐⭐)

CPP
#include <iostream>
#include <vector>
#include <thread>
#include <algorithm>

void parallelSort(std::vector<int>& v, int left, int right) {
 if (left >= right) return;
 
 std::sort(v.begin() + left, v.begin() + right + 1);
}

int main() {
 std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
 
 // Sort in parallel with 2 threads
 std::thread t1(parallelSort, std::ref(v), 0, v.size()/2);
 std::thread t2(parallelSort, std::ref(v), v.size()/2 + 1, v.size()-1);
 
 t1.join();
 t2.join();
 
 // Merge the two sorted halves
 std::inplace_merge(v.begin(), v.begin() + v.size()/2 + 1, v.end());
 
 for (int x : v) {
 std::cout << x << " ";
 }
 std::cout << std::endl;
 
 return 0;
}
▶ Try it Yourself

Output:

TEXT 📖 Display only
3 1 4 1 5 9 2 6

▶ Example 3: Protecting a shared counter with mutex (Difficulty ⭐)

CPP
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>

int counter = 0;
std::mutex mtx;

void increment() {
    for (int i = 0; i < 1000; i++) {
        std::lock_guard<std::mutex> lock(mtx);
        counter++;
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; i++) {
        threads.emplace_back(increment);
    }

    for (auto& t : threads) {
        t.join();
    }

    std::cout << "counter = " << counter << std::endl;
    return 0;
}
▶ Try it Yourself

Output:

TEXT 📖 Display only
counter = 10000

❓ FAQ

Q Is multithreading always faster?
A Not necessarily. If tasks are too small, the overhead of thread creation and synchronization may exceed the benefit.

Q How do I debug multithreaded programs?
A - Use std::cout to print logs (but add a mutex) - Use Valgrind, Helgrind, etc. to detect data races - Use Thread Sanitizer (GCC/Clang)

Q What's new in C++20?
A - std::jthread (auto-join) - Semaphores (std::counting_semaphore) - Latches (std::latch) - Barriers (std::barrier)

📖 Summary

Key Point Summary
Deadlock Four conditions, three avoidance methods
Read-write lock std::shared_mutex (C++17)
Atomic operations std::atomic, no lock needed
Thread pool Reuse threads, reduce overhead
Parallel algorithms C++17 std::execution

📝 Exercises

  1. Basic (Difficulty ⭐): Create two threads that each increment a global variable 100,000 times. Observe if the result is correct. Then protect it with std::mutex and test again.

  2. Intermediate (Difficulty ⭐⭐): Use std::lock_guard to protect an "account balance" variable. Simulate two threads performing deposit/withdrawal operations simultaneously, ensuring the final balance is correct.

  3. Challenge (Difficulty ⭐⭐⭐): Use std::condition_variable to implement a "producer-consumer" model. One thread produces data and puts it in a queue, another thread consumes data. The consumer waits when the queue is empty.



Phase 6 Complete! Next: Phase 7 (Advanced & Practice)

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏