c++语法复习
整理
1. 主要锁类型对比
| 锁类型 | 特点 | 适用场景 |
|---|---|---|
std::lock_guard | RAII风格,构造时自动加锁,析构时自动解锁,不支持手动操作 | 简单场景,锁的作用范围明确且不需要中途解锁 |
std::unique_lock | RAII风格,支持手动加锁/解锁、延迟加锁、超时操作等 | 需要灵活控制锁的场景,如条件变量等待 |
std::scoped_lock (C++17) | 可同时管理多个锁,自动处理锁的获取顺序以避免死锁 | 需要同时获取多个锁的场景 |
std::shared_lock (C++17) | 与std::shared_mutex配合使用,实现共享所有权 | 读写分离场景,允许多个读者同时访问 |
2. 各类锁的基本用法
2.1 std::lock_guard
std::lock_guard是最简单的锁管理类,采用RAII模式,在构造时获取锁,在析构时释放锁,确保锁的正确释放。
#include <mutex>
std::mutex mtx;
int shared_data = 0;
void increment() {
// 构造时自动加锁
std::lock_guard<std::mutex> lock(mtx);
// 临界区操作
shared_data++;
// 函数结束时,lock对象析构,自动解锁
}
2.2 std::unique_lock
std::unique_lock提供了更灵活的锁管理,可以手动控制锁的状态,支持延迟加锁、超时等操作。
#include <mutex>
std::mutex mtx;
int shared_data = 0;
void operation() {
// 延迟加锁:构造时不获取锁
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// 执行不需要锁的操作
prepare_data();
// 手动加锁
lock.lock();
// 临界区操作
shared_data++;
// 可以手动解锁
lock.unlock();
// 执行其他不需要锁的操作
process_results();
// 可以再次加锁
lock.lock();
shared_data++;
}
2.3 std::scoped_lock (C++17)
std::scoped_lock用于同时管理多个锁,自动处理锁的获取顺序,避免死锁。
#include <mutex>
std::mutex mtx1, mtx2;
int data1 = 0, data2 = 0;
void transfer() {
// 同时获取多个锁,避免死锁
std::scoped_lock lock(mtx1, mtx2);
// 操作需要多个锁保护的资源
data1--;
data2++;
}
2.4 std::shared_lock (C++17)
std::shared_lock与std::shared_mutex配合使用,实现读写分离,允许多个读者同时访问,但写操作是独占的。
#include <shared_mutex>
#include <vector>
std::shared_mutex smtx;
std::vector<int> data;
// 读操作:多个线程可同时执行
int get_data(int index) {
std::shared_lock<std::shared_mutex> lock(smtx);
return data[index];
}
// 写操作:仅一个线程可执行
void add_data(int value) {
std::unique_lock<std::shared_mutex> lock(smtx);
data.push_back(value);
}
3. 并发操作的核心原则
3.1 最小化临界区
只在必要时持有锁,减少锁的持有时间,提高并发性能:
// 不好的做法:锁持有时间过长
std::lock_guard<std::mutex> lock(mtx);
read_input(); // 不需要锁的操作
process_data(); // 需要锁的操作
write_output(); // 不需要锁的操作
// 好的做法:只在必要时持有锁
read_input(); // 不需要锁的操作
{
std::lock_guard<std::mutex> lock(mtx);
process_data(); // 需要锁的操作
}
write_output(); // 不需要锁的操作
3.2 避免死锁
获取多个锁时,确保所有线程以相同的顺序获取锁:
// 线程1
std::lock_guard<std::mutex> lock1(mtx1);
std::lock_guard<std::mutex> lock2(mtx2);
// 线程2 - 错误做法(顺序不同)
std::lock_guard<std::mutex> lock2(mtx2);
std::lock_guard<std::mutex> lock1(mtx1);
// 线程2 - 正确做法(与线程1顺序相同)
std::lock_guard<std::mutex> lock1(mtx1);
std::lock_guard<std::mutex> lock2(mtx2);
或者使用`std::scoped_lock`同时获取多个锁:
std::scoped_lock lock(mtx1, mtx2); // 自动处理顺序,避免死锁
3.3 使用条件变量进行同步
std::condition_variable用于线程间的等待/通知机制,配合std::unique_lock使用:
#include <condition_variable>
#include <mutex>
#include <queue>
std::queue<int> work_queue;
std::mutex mtx;
std::condition_variable cv;
bool done = false;
void worker() {
while (!done) {
std::unique_lock<std::mutex> lock(mtx);
// 等待队列非空或任务完成
cv.wait(lock, []{ return !work_queue.empty() || done; });
if (done) break;
int task = work_queue.front();
work_queue.pop();
lock.unlock(); // 提前解锁
process_task(task);
}
}
void producer() {
for (int i = 0; i < 10; ++i) {
{
std::lock_guard<std::mutex> lock(mtx);
work_queue.push(i);
}
cv.notify_one(); // 通知工作线程
}
// 标记任务完成
{
std::lock_guard<std::mutex> lock(mtx);
done = true;
}
cv.notify_all(); // 唤醒所有工作线程
}
4. 锁的选择策略
- 简单场景优先使用
std::lock_guard,它更轻量且不易出错 - 需要灵活控制锁的状态时使用
std::unique_lock,如条件变量等待 - 需要同时获取多个锁时使用
std::scoped_lock - 读写分离场景使用
std::shared_mutex配合std::shared_lock和std::unique_lock
选择合适的锁类型可以提高代码的可读性、正确性和性能。在实际开发中,应根据具体场景选择最适合的锁机制。
1. std::unique_lock 详解
1.1 基本概念
std::unique_lock是一个通用的锁管理类模板,它提供了对互斥锁的灵活管理,实现了RAII(资源获取即初始化)机制,确保锁能被正确释放。
与std::lock_guard相比,std::unique_lock提供了更多功能,但也带来了一些性能开销。
1.2 基础用法
#include <mutex>
#include <iostream>
std::mutex mtx;
int shared_value = 0;
void increment() {
// 构造时自动获取锁
std::unique_lock<std::mutex> lock(mtx);
// 临界区操作
shared_value++;
std::cout << "Value: " << shared_value << std::endl;
// 析构时自动释放锁
}
int main() {
increment();
return 0;
}
1.3 进阶用法
延迟加锁
// 延迟加锁:构造时不获取锁
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// 执行不需要锁的操作
prepare_data();
// 手动获取锁
lock.lock();
// 临界区操作
process_data();
// 手动释放锁(可选)
lock.unlock();
// 执行其他不需要锁的操作
cleanup();
#### 尝试加锁
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
if (lock.try_lock()) {
// 成功获取锁,执行操作
process_data();
} else {
// 未能获取锁,执行备选方案
handle_lock_failure();
}
超时加锁
#include <chrono>
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// 尝试在1秒内获取锁
if (lock.try_lock_for(std::chrono::seconds(1))) {
// 成功获取锁
} else {
// 超时
}
// 或在指定时间点前尝试获取锁
auto timeout_time = std::chrono::system_clock::now() + std::chrono::seconds(1);
if (lock.try_lock_until(timeout_time)) {
// 成功获取锁
} else {
// 超时
}
1.4 常用方法
| 方法 | 描述 |
|---|---|
lock() | 获取锁,若无法获取则阻塞 |
unlock() | 释放锁 |
try_lock() | 尝试获取锁,成功返回true,否则返回false |
try_lock_for(duration) | 在指定时间段内尝试获取锁 |
try_lock_until(time_point) | 在指定时间点前尝试获取锁 |
owns_lock() | 检查是否持有锁 |
release() | 释放对锁的所有权,返回指向锁的指针 |
2. std::condition_variable 详解
2.1 基本概念
std::condition_variable用于线程间的同步,允许一个线程等待另一个线程发送的通知。它必须与std::unique_lock配合使用。
条件变量解决了线程间"等待-通知"的同步问题,避免了忙等待(busy waiting)带来的性能消耗。
2.2 基础用法
#include <condition_variable>
#include <mutex>
#include <thread>
#include <iostream>
std::condition_variable cv;
std::mutex mtx;
bool data_ready = false;
int shared_data;
// 等待数据准备好
void reader() {
std::unique_lock<std::mutex> lock(mtx);
// 等待直到数据准备好
cv.wait(lock, []{ return data_ready; });
// 处理数据
std::cout << "Received data: " << shared_data << std::endl;
data_ready = false;
}
// 准备数据并通知
void writer() {
{
std::lock_guard<std::mutex> lock(mtx);
shared_data = 42;
data_ready = true;
} // 提前解锁以提高性能
// 通知等待的线程
cv.notify_one();
}
int main() {
std::thread t1(reader);
std::thread t2(writer);
t1.join();
t2.join();
return 0;
}
2.3 进阶用法
超时等待
// 等待最多1秒
std::unique_lock<std::mutex> lock(mtx);
if (cv.wait_for(lock, std::chrono::seconds(1), []{ return data_ready; })) {
// 条件满足
process_data();
} else {
// 超时处理
handle_timeout();
}
通知所有线程
// 通知所有等待的线程 cv.notify_all();
2.4 注意事项
- 始终使用带谓词的
wait版本,以处理虚假唤醒(spurious wakeups) - 在调用
notify_one()或notify_all()前确保已修改共享状态 - 条件变量必须与
std::unique_lock<std::mutex>配合使用 - 避免在持有锁时进行耗时操作
3. 应用场景
3.1 生产者-消费者模型
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>
std::queue<int> data_queue;
std::mutex mtx;
std::condition_variable cv;
bool done = false;
// 生产者
void producer() {
for (int i = 0; i < 10; ++i) {
{
std::lock_guard<std::mutex> lock(mtx);
data_queue.push(i);
std::cout << "Produced: " << i << std::endl;
}
cv.notify_one(); // 通知消费者
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// 标记生产完成
{
std::lock_guard<std::mutex> lock(mtx);
done = true;
}
cv.notify_all();
}
// 消费者
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// 等待队列非空或生产完成
cv.wait(lock, []{ return !data_queue.empty() || done; });
// 如果生产完成且队列为空,则退出
if (done && data_queue.empty()) break;
// 处理数据
int data = data_queue.front();
data_queue.pop();
lock.unlock();
std::cout << "Consumed: " << data << std::endl;
}
}
int main() {
std::thread prod(producer);
std::thread cons1(consumer);
std::thread cons2(consumer);
prod.join();
cons1.join();
cons2.join();
return 0;
}
3.2 线程池实现
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <iostream>
class ThreadPool {
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
bool stop;
public:
// 构造函数:创建n个工作线程
ThreadPool(size_t n) : stop(false) {
for (size_t i = 0; i < n; ++i) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->mtx);
// 等待任务或停止信号
this->cv.wait(lock, [this] {
return this->stop || !this->tasks.empty();
});
// 如果停止且没有任务,退出线程
if (this->stop && this->tasks.empty())
return;
// 获取任务
task = std::move(this->tasks.front());
this->tasks.pop();
}
// 执行任务
task();
}
});
}
}
// 添加任务
template<class F>
void enqueue(F&& f) {
{
std::lock_guard<std::mutex> lock(mtx);
tasks.emplace(std::forward<F>(f));
}
cv.notify_one(); // 通知一个工作线程
}
// 析构函数:停止所有线程
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(mtx);
stop = true;
}
cv.notify_all(); // 唤醒所有工作线程
// 等待所有线程完成
for (std::thread& worker : workers)
worker.join();
}
};
// 使用示例
int main() {
ThreadPool pool(4); // 创建4个线程的线程池
// 添加任务
for (int i = 0; i < 8; ++i) {
pool.enqueue([i] {
std::cout << "Task " << i << " executed by thread "
<< std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
});
}
return 0;
}
4. 最佳实践
- 优先使用RAII:始终通过
std::unique_lock管理锁的生命周期,避免手动调用lock()和unlock() - 最小化锁持有时间:只在必要时持有锁,完成临界区操作后立即释放
- 处理虚假唤醒:始终使用带谓词的
wait版本,如cv.wait(lock, predicate) - 避免在持有锁时调用未知函数:防止死锁和性能问题
- 适当使用通知机制:在多消费者场景,
notify_one()通常比notify_all()更高效 - 注意线程安全的初始化:确保共享数据在被多个线程访问前正确初始化