c++语法复习

整理

1. 主要锁类型对比

锁类型特点适用场景
std::lock_guardRAII风格,构造时自动加锁,析构时自动解锁,不支持手动操作简单场景,锁的作用范围明确且不需要中途解锁
std::unique_lockRAII风格,支持手动加锁/解锁、延迟加锁、超时操作等需要灵活控制锁的场景,如条件变量等待
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_lockstd::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. 锁的选择策略

  1. 简单场景优先使用std::lock_guard,它更轻量且不易出错
  2. 需要灵活控制锁的状态时使用std::unique_lock,如条件变量等待
  3. 需要同时获取多个锁时使用std::scoped_lock
  4. 读写分离场景使用std::shared_mutex配合std::shared_lockstd::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 注意事项

  1. 始终使用带谓词的wait版本,以处理虚假唤醒(spurious wakeups)
  2. 在调用notify_one()notify_all()前确保已修改共享状态
  3. 条件变量必须与std::unique_lock<std::mutex>配合使用
  4. 避免在持有锁时进行耗时操作

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. 最佳实践

  1. 优先使用RAII:始终通过std::unique_lock管理锁的生命周期,避免手动调用lock()unlock()
  2. 最小化锁持有时间:只在必要时持有锁,完成临界区操作后立即释放
  3. 处理虚假唤醒:始终使用带谓词的wait版本,如cv.wait(lock, predicate)
  4. 避免在持有锁时调用未知函数:防止死锁和性能问题
  5. 适当使用通知机制:在多消费者场景,notify_one()通常比notify_all()更高效
  6. 注意线程安全的初始化:确保共享数据在被多个线程访问前正确初始化