同步机制用于协调多个线程堆共享资源的访问。
互斥锁mutex
std::mutex是C++中最基础的互斥锁,需要引入头文件<mutex>,它保证同一时刻只能有一个线程能够进入临界区。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include <iostream> #include <thread> #include <mutex>
int counter = 0; std::mutex mtx;
void add() { for (int i = 0; i < 100000; ++i) { mtx.lock(); ++counter; mtx.unlock(); } }
int main() { std::thread t1(add); std::thread t2(add);
t1.join(); t2.join();
std::cout << counter << std::endl; return 0; }
|
此时结果稳定为:
但是手动调用 lock() 和 unlock() 有风险。
如果临界区中发生异常,可能导致锁无法释放。
lock_guard
std::lock_guard是 RAII 风格的加锁工具,对象创建时自动加锁,对象析构时自动解锁,建议优先使用。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <iostream> #include <thread> #include <mutex>
int counter = 0; std::mutex mtx;
void add() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lock(mtx); ++counter; } }
int main() { std::thread t1(add); std::thread t2(add);
t1.join(); t2.join();
std::cout << counter << std::endl; return 0; }
|
优点:
unique_lock
std::unique_lock比std::lock_guard更灵活。
它支持:
- 延迟加锁
- 手动解锁
- 转移所有权
- 可配合条件变量使用
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include <iostream> #include <thread> #include <mutex>
std::mutex mtx;
void task() { std::unique_lock<std::mutex> lock(mtx);
std::cout << "线程正在执行临界区代码" << std::endl;
lock.unlock();
std::cout << "线程执行非临界区代码" << std::endl; }
int main() { std::thread t1(task); std::thread t2(task);
t1.join(); t2.join();
return 0; }
|
recursive_mutex
std::recursive_mutex允许同一个线程多次获得同一把锁,普通std::mutex如果同一个线程重复加锁,会导致锁死。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <iostream> #include <mutex>
std::recursive_mutex rmtx;
void func(int n) { if (n <= 0) return;
std::lock_guard<std::recursive_mutex> lock(rmtx); std::cout << "n = " << n << std::endl;
func(n - 1); }
int main() { func(3); return 0; }
|
注意:
虽然 recursive_mutex 可以避免递归调用时死锁,但不应滥用。
通常更推荐重新设计代码结构。
timed_mutex
std::timed_mutex支持尝试在一定时间内加锁。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <iostream> #include <thread> #include <mutex> #include <chrono>
std::timed_mutex tmtx;
void task(int id) { if (tmtx.try_lock_for(std::chrono::seconds(1))) { std::cout << "线程 " << id << " 获得锁" << std::endl; std::this_thread::sleep_for(std::chrono::seconds(2)); tmtx.unlock(); } else { std::cout << "线程 " << id << " 获取锁超时" << std::endl; } }
int main() { std::thread t1(task, 1); std::thread t2(task, 2);
t1.join(); t2.join();
return 0; }
|
条件变量 condition_variable
条件变量用于线程之间的等待和通知。
常用于:
- 生产者消费者模型
- 任务队列
- 线程池
- 等待某个状态变化
基本函数
1
| std::condition_variable cv;
|
常用操作:
1 2 3 4
| cv.wait(lock); cv.wait(lock, predicate); cv.notify_one(); cv.notify_all();
|
生产者消费者示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <queue>
std::queue<int> q; std::mutex mtx; std::condition_variable cv;
void producer() { for (int i = 1; i <= 5; ++i) { { std::lock_guard<std::mutex> lock(mtx); q.push(i); std::cout << "生产: " << i << std::endl; }
cv.notify_one(); } }
void consumer() { for (int i = 1; i <= 5; ++i) { std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !q.empty(); });
int value = q.front(); q.pop();
std::cout << "消费: " << value << std::endl; } }
int main() { std::thread t1(producer); std::thread t2(consumer);
t1.join(); t2.join();
return 0; }
|
为什么 wait 要配合条件判断?
推荐写法:
1 2 3
| cv.wait(lock, [] { return !q.empty(); });
|
原因:
- 防止虚假唤醒
- 避免条件不满足时继续执行
- 代码更安全
原子操作 atomic
std::atomic用于无锁地操作共享变量。
适合简单的数据类型,例如:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <iostream> #include <thread> #include <atomic>
std::atomic<int> counter{0};
void add() { for (int i = 0; i < 100000; ++i) { ++counter; } }
int main() { std::thread t1(add); std::thread t2(add);
t1.join(); t2.join();
std::cout << counter << std::endl; return 0; }
|
输出稳定为:
atomic 与 mutex 的区别
| 对比项 |
atomic |
mutex |
| 适用场景 |
简单变量操作 |
复杂临界区 |
| 性能 |
通常较高 |
有锁开销 |
| 使用难度 |
中等 |
简单直观 |
| 是否阻塞 |
通常不阻塞 |
可能阻塞 |
| 表达能力 |
较弱 |
较强 |
CAS
CAS(Compare-And-Swap / Compare-And-Exchange,比较并交换)是并发编程中的一种原子操作,常用于实现无锁数据结构、原子计数器、自旋锁等。
死锁
死锁指多个线程互相等待对方释放资源,导致程序永久阻塞。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream> #include <thread> #include <mutex>
std::mutex m1; std::mutex m2;
void thread1() { std::lock_guard<std::mutex> lock1(m1); std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::lock_guard<std::mutex> lock2(m2);
std::cout << "thread1 done" << std::endl; }
void thread2() { std::lock_guard<std::mutex> lock1(m2); std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::lock_guard<std::mutex> lock2(m1);
std::cout << "thread2 done" << std::endl; }
int main() { std::thread t1(thread1); std::thread t2(thread2);
t1.join(); t2.join();
return 0; }
|
这里可能发生死锁:
1 2
| 线程 1 持有 m1,等待 m2 线程 2 持有 m2,等待 m1
|
避免死锁的方法
方法一:固定加锁顺序
所有线程都按相同顺序加锁。
1 2
| std::lock_guard<std::mutex> lock1(m1); std::lock_guard<std::mutex> lock2(m2);
|
方法二:使用 std::lock
std::lock 可以一次性锁住多个 mutex,避免死锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include <iostream> #include <thread> #include <mutex>
std::mutex m1; std::mutex m2;
void task() { std::lock(m1, m2);
std::lock_guard<std::mutex> lock1(m1, std::adopt_lock); std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);
std::cout << "任务完成" << std::endl; }
int main() { std::thread t1(task); std::thread t2(task);
t1.join(); t2.join();
return 0; }
|
方法三:使用 scoped_lock
C++17 推荐使用 std::scoped_lock。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <iostream> #include <thread> #include <mutex>
std::mutex m1; std::mutex m2;
void task() { std::scoped_lock lock(m1, m2);
std::cout << "任务完成" << std::endl; }
int main() { std::thread t1(task); std::thread t2(task);
t1.join(); t2.join();
return 0; }
|