互斥锁
互斥锁保证某一时刻,只能在一处获取锁,当当前线程拥有锁时,其他线程如果想获取锁,那么必须等待直到对方解锁。C++11中的互斥锁:std::mutex, std::lock_guard, std::unique_lock。
std::mutex
C++11中定义的互斥量,实现互斥锁的功能,即同一时刻只能有一个线程获取该锁。底层的实现原理是包装了pthread_mutex_t结构体,并调用pthread_mutex_lock和pthread_mutex_unlock完成加锁和解锁的功能。其内存布局:
typedef union {
struct __pthread_mutex_s {
int __lock; //!< mutex状态,0:未占用;1:占用。
unsigned int __count; //!< 可重入锁时,持有线程上锁的次数。
int __owner; //!< 持有线程的线程ID。
unsigned int __nusers;
int __kind; //!< 上锁类型。
int __spins;
__pthread_list_t __list;
} __data;
} pthread_mutex_t;
其中_kind为上锁类型,有如下几种取值:
- PTHREAD_MUTEX_TIMED_NP ,这是缺省值,也就是普通锁。
- PTHREAD_MUTEX_RECURSIVE_NP ,可重入锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
- PTHREAD_MUTEX_ERRORCHECK_NP ,检错锁,如果同一个线程重复请求同一个锁,则返回 EDEADLK ,否则与 PTHREAD_MUTEX_TIMED_NP 类型相同。
- PTHREAD_MUTEX_ADAPTIVE_NP ,自适应锁,自旋锁与普通锁的混合。
更多关于mutex解锁加锁的实现
std::lock_guard / std::unique_lock
这两个类都是用了RAII(Resource Acquire is Initialized)机制对mutex资源进行包装。std::lock_guard是在创建对象时就对mutex对象进行加锁,当离开作用域时,调用析构函数,自动解锁,lock_guard只有构造函数和析构函数,使用起来简单。std::unique_lock对mutex的所有操作都进行了封装,包括加锁、解锁操作、延迟加锁、递归加锁等。std::unique_lock与std::lock_guard一样都是在创建时加锁,销毁时解锁。unique_lock在创建时能指定加锁方式。try_to_lock
获取锁,如果锁不可获取,直接返回;defer_lock
延迟加锁
使用示例
class Foo {
mutex mtx1, mtx2;
unique_lock<mutex> lock1, lock2;
Foo() : lock1(mtx1, try_to_lock), lock2(mtx2, try_to_lock) {}
void first(std::function<void ()> printFirst) {
printFirst();
lock1.unlock();
}
void second(std::function<void ()> printSecond) {
lock_guard<mutex> guard(mtx1); // 尝试获取锁1
printSecond();
lock2.unlock();
}
void third(std::function<void ()> printThird) {
lock_guard<mutex> gurad(mtx2) // 尝试获取锁2
printThird();
}
};
条件变量
条件变量是一种线程同步机制,条件变量通常与互斥锁一起使用,互斥锁上锁,条件变量用于在多线程环境中等待特定事件发生。当某个特定事件满足后,线程会发出通知,唤醒等待队列上的所有线程,唤醒的线程会判断自身的条件是否满足,如果满足,获取锁,执行函数体,然后唤醒下一个等待线程,使程序以同步的方式执行。
std::condition_variable
std::condition_variable
是一种用来同时阻塞多个线程的同步原语(synchronization primitive),std::condition_variable
必须和 std::unique_lock
搭配使用。
使用示例
class Foo {
mutex mtx1, mtx2;
std::condition_variable cond1;
int k = 0;
Foo() {}
void first(std::function<void ()> printFirst) {
printFirst();
k = 1;
cond1.notify_all();
}
void second(std::function<void ()> printSecond) {
lock_guard<mutex> guard(mtx1); // 尝试获取锁1
cond1.wait(guard, []() { return k == 1});
printSecond();
k = 2;
cond1.notify_one();
}
void third(std::function<void ()> printThird) {
lock_guard<mutex> gurad2(mtx2) // 尝试获取锁2
cond1.wait(guard2, []() {return k == 2});
printThird();
}
};
信号量
信号量是用来实现对共享资源的同步访问的机制,其使用方法和条件变量类似,都是通过主动等待和主动唤醒来实现的。
在C++下使用信号量实际上是使用C语言的信号量,使用<sempahore.h>库。
#include <sempahore.h>
class Foo {
sem_t sem_1, sem_2;
Foo() {
sem_init(&sem_1, 0, 0);
sem_init(&sem2, 0, 0);
}
void first(std::function<void ()> printFirst) {
printFirst();
sem_post(&sem_1); // 发射信号量1
}
void second(std::function<void ()> printSecond) {
sem_wait(&sem_1); // 等待信号量1
printSecond();
sem_post(&sem_2); // 发射信号量2
}
void third(std::function<void ()> printThird) {
sem_wait(&sem_2); // 等待信号量2
printThird();
}
};
异步操作
基于 future/promise 的异步编程模型
原子操作
我们平时进行的数据修改都是非原子操作,如果多个线程同时以非原子操作的方式修改同一个对象可能会发生数据争用,从而导致未定义行为;而原子操作能够保证多个线程顺序访问,不会导致数据争用,其执行时没有任何其它线程能够修改相同的原子对象。C++中可以使用std::atomic来定义原子变量。
class Foo {
std::atomic<bool> first {false};
std::atomic<bool> second{false};
Foo() {
}
void first(std::function<void ()> printFirst) {
printFirst();
first = true;
}
void second(std::function<void ()> printSecond) {
while (!first)
this_thread::sleep_for(chrono::millisecond(1));
printSecond();
second = true;
}
void third(std::function<void ()> printThird) {
while (!second)
this_thread::sleep_for(chrono::millisecond(1));
printThird();
}
};