1、互斥锁
根据 C++ 标准,在一个线程尝试对一个 mutex 对象进行 unlock 操作时,mutex 对象的所有权必须在这个线程上;也就是说,应该 由同一个线程来对一个 mutex 对象进行 lock 和 unlock 操作,否则会产生未定义行为。
基于以上背景,因为 mutex 对象本身是不保护任何数据的,我们只是通过 mutex 的机制来保护数据被同时访问,所以最好 使用 lock_guard 或者 unique_lock 提供的 RAII 机制来管理 mutex 对象,而不是直接操作 mutex 对象;其中 lock_guard 只拥有构造和析构函数,用来实现 RAII 机制,而 unique_lock 是一个完整的 mutex 所有权包装器,封装了所有 mutex 的函数。
RAII是Resource Acquisition Is Initialization(wiki上面翻译成 “资源获取就是初始化”)的简称,是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。
这两种锁都可以对std::mutex进行封装,实现RAII的效果。绝大多数情况下这两种锁是可以互相替代的,区别是unique_lock比lock_guard能提供更多的功能特性(但需要付出性能的一些代价),如下:
1、unique_lock可以实现延时锁,即先生成unique_lock对象,然后在有需要的地方调用lock函数,lock_guard在对象创建时就自动进行lock操作了;
2、unique_lock可以在需要的地方调用unlock操作,而lock_guard只能在其对象生命周期结束后自动Unlock;
正是由于这两个差异特性,unique_lock可以用于一次性锁多个锁以及用于条件变量的搭配使用,而lock_guard做不到。
class Foo {
mutex mtx_1, mtx_2;
unique_lock<mutex> lock_1, lock_2;
public:
Foo() : lock_1(mtx_1, try_to_lock), lock_2(mtx_2, try_to_lock) {
}
void first(function<void()> printFirst) {
printFirst();
lock_1.unlock();
}
void second(function<void()> printSecond) {
lock_guard<mutex> guard(mtx_1);
printSecond();
lock_2.unlock();
}
void third(function<void()> printThird) {
lock_guard<mutex> guard(mtx_2);
printThird();
}
};
2、条件变量
利用条件变量cv
来控制修改特定变量k
,进而去通知条件变量,唤醒下一个函数继续执行。
`std::condition_variable` 是一种用来同时阻塞多个线程的**同步原语**(synchronization primitive),**`std::condition_variable` 必须和 `std::unique_lock` 搭配使用**:
class Foo {
condition_variable cv;
mutex mtx;
int k = 0;
public:
void first(function<void()> printFirst) {
printFirst();
k = 1;
cv.notify_all(); // 通知其他所有在等待唤醒队列中的线程
}
void second(function<void()> printSecond) {
unique_lock<mutex> lock(mtx); // lock mtx
cv.wait(lock, [this](){ return k == 1; }); // unlock mtx,并阻塞等待唤醒通知,需要满足 k == 1 才能继续运行
printSecond();
k = 2;
cv.notify_one(); // 随机通知一个(unspecified)在等待唤醒队列中的线程
}
void third(function<void()> printThird) {
unique_lock<mutex> lock(mtx); // lock mtx
cv.wait(lock, [this](){ return k == 2; }); // unlock mtx,并阻塞等待唤醒通知,需要满足 k == 2 才能继续运行
printThird();
}
};
template<typename _Predicate>
void wait(unique_lock<mutex>& __lock, _Predicate __p)
{
while (!__p())
wait(__lock);
}
3、信号量
C++ 标准库中并没有信号量的实现和封装,我们可以用 C 语言提供的 <sempahore.h> 库来解决。
#include <semaphore.h>
class Foo {
private:
sem_t sem_1, sem_2;
public:
Foo() {
sem_init(&sem_1, 0, 0), sem_init(&sem_2, 0, 0);
}
void first(function<void()> printFirst) {
printFirst();
sem_post(&sem_1);
}
void second(function<void()> printSecond) {
sem_wait(&sem_1);
printSecond();
sem_post(&sem_2);
}
void third(function<void()> printThird) {
sem_wait(&sem_2);
printThird();
}
};
4、原子操作
我们平时进行的数据修改都是非原子操作,如果多个线程同时以非原子操作的方式修改同一个对象可能会发生数据争用,从而导致未定义行为;而 原子操作能够保证多个线程顺序访问,不会导致数据争用,其执行时没有任何其它线程能够修改相同的原子对象。
class Foo {
std::atomic<bool> a{ false };
std::atomic<bool> b{ false };
public:
void first(function<void()> printFirst) {
printFirst();
a = true;
}
void second(function<void()> printSecond) {
while (!a)
this_thread::sleep_for(chrono::milliseconds(1));//休眠一毫秒
printSecond();
b = true;
}
void third(function<void()> printThird) {
while (!b)
this_thread::sleep_for(chrono::milliseconds(1));
printThird();
}
};