C++ 多线程中的同步方法

1. 线程同步

标准库中的同步方法都是 RAII 的。

std::mutex 互斥锁,防止多个线程同时访问共享资源。
在C代码中往往会直接使用pthread线程库中的mutex,但是在C++中一般使用lock_guard或者unique_lock。
std::lock_guard RAII 风格的锁管理器,离开作用域自动释放锁。
相对于std::unique_lock更加轻量级,用于简单场景。
构造时立即加锁,析构时自动解锁。
不支持手动解锁或重新加锁。
不可复制,不可移动。
std::unique_lock 功能类似lock_guard,功能更多。
支持延迟加锁(通过 std::defer_lock)。
支持手动调用 lock() 和 unlock()。
支持尝试加锁(try_lock()、try_lock_for()、try_lock_until())。
可移动(std::move),不可复制。
std::atomic 原子操作模板类型,基于硬件级别的原子指令,用于简单数据类型,比如说基本数据类型的自增,自减。

2. 互斥锁 (std::mutex)

其中最基础的就是互斥锁了, 用来保证同一时刻只有一个线程访问临界区代码,通常配合 std::lock_guardstd::unique_lock 使用。std::lock_guard的功能非常简单,就是创建一个 RAII 风格的锁管理器,在作用域结束后自动销毁锁。
此处主要介绍std::unique_lock:

  1. 延迟加锁(std::defer_lock),在构造 std::unique_lock 时不立即加锁,而是稍后手动加锁。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx1, mtx2;

void task() {
    std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock); // 不立即加锁
    std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock); // 不立即加锁

    // 手动加锁,避免死锁(按固定顺序)
    std::lock(lock1, lock2); // 安全地同时加锁两个互斥量

    // 访问共享资源
    std::cout << "Thread acquired both locks." << std::endl;

    // 离开作用域,自动销毁锁
}

  1. 手动加锁/解锁,在函数内部临时执行共享数据的互斥读/写。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;
int shared_data = 0;

void process_data() {
    // ...
    std::unique_lock<std::mutex> lock(mtx);   // 上锁
    ++shared_data;                            // 读写共享数据
    lock.unlock();                            // 解锁
    // ...
}

  1. 尝试加锁(try_lock),避免线程长时间等待锁,提高程序响应性。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;

void attempt_lock() {
    std::unique_lock<std::mutex> lock(mtx, std::try_to_lock); // 尝试加锁

    if (lock.owns_lock()) {
        std::cout << "Lock acquired successfully!" << std::endl;
        // 访问共享资源
    } else {
        std::cout << "Failed to acquire lock. Doing something else..." << std::endl;
    }
}

  1. 与条件变量配合,线程等待某个条件满足后被唤醒,实现线程间的同步(如生产者-消费者模型)。
 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
#include <iostream>
#include <mutex>
#include <condition_variable>
#include <thread>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void wait_for_ready() {
    std::unique_lock<std::mutex> lock(mtx);
    /**
     * 1. 自动解锁,并等待信通知。   
     * 2. 线程被唤醒时,会自动重新获取锁,确保后续操作的原子性。  
      */
    cv.wait(lock, []{ return ready; });                         
    std::cout << "Condition met! Proceeding..." << std::endl;
}

void set_ready() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_one();   // 唤醒一个等待线程
}

int main() {
    std::thread t1(wait_for_ready);
    std::thread t2(set_ready);

    t1.join();
    t2.join();
    return 0;
}

  1. 转移锁所有权(移动语义),在函数间传递锁的管理权。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;

void transfer_lock(std::unique_lock<std::mutex> lock) {
    std::cout << "Lock transferred to this function." << std::endl;
    // 函数结束时自动解锁
}

int main() {
    std::unique_lock<std::mutex> lock(mtx);
    transfer_lock(std::move(lock)); // 移动锁的所有权
    // 此时 lock 为空,不能再使用
    return 0;
}

3. 避免死锁

std::scoped_lock 支持同时锁定多个互斥锁,并通过原子性加锁(内部调用 std::lock)避免死锁。

1
2
3
4
5
6
7
8
9
#include <mutex>
#include <thread>

std::mutex mtx1, mtx2;

void update_shared_resources() {
    std::scoped_lock lock(mtx1, mtx2); // 同时锁定两个互斥锁
    // 安全地访问共享资源
}

4. 使用消息队列

共享变量越多,锁的复杂度越高,所以尽可能使用消息队列进行线程间通信。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
std::queue<int> tasks;
std::mutex mtx;
std::condition_variable cv;

void producer() {
    std::lock_guard<std::mutex> lock(mtx);
    tasks.push(42);                          //  入队
    cv.notify_one();                         //  通知消费者
}

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return !tasks.empty(); });  //  等待队列非空
    int task = tasks.front();                      //  读取队头数据
    tasks.pop();  
}

5. 读写锁(std::shared_mutex)

现在有一个多线程场景,一个线程写数据,另外还有多个线程读数据,那么 std::shared_mutex 就派上了用场了, 这是 C++17 提供的特性,允许多个线程同时读,但写操作必须独占。搭配 std::shared_lockstd::unique_lock 使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <shared_mutex>
#include <thread>
#include <vector>
#include <iostream>

std::shared_mutex rw_mtx;

void reader(int id) {
    std::shared_lock<std::shared_mutex> lock(rw_mtx);
    std::cout << "Reader 线程读数据。" << std::endl;
}

void writer(int val) {
    std::unique_lock<std::shared_mutex> lock(rw_mtx);
    std::cout << "Writer 线程写数据 " << std::endl;
}

6. 原子操作(atomic)

可以理解为一个非常简单的计数器。可以原子的递增、递减。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> counter{0};

void worker() {
    for (int i = 0; i < 10000; ++i) {
        counter.fetch_add(1);
    }
}

int main() {
    std::thread t1(worker);
    std::thread t2(worker);
    t1.join();
    t2.join();
    std::cout << "counter = " << counter << std::endl;
}
comments powered by Disqus