0%

CPP中的线程同步机制

锁机制原理

锁机制源于信号量,其实现与操作系统息息相关。一般而言,线程存在下图所示的5种状态,其中当线程处于阻塞状态时,不会调度到处理机中运行。

线程状态

信号量机制则为在需互斥线程间共享一个数据,同时提供PV两种操作,两种操作的行为如下:

  1. P操作:将数据减一,当数据小于零时,将当前线程阻塞。

  2. V操作:将数据加一,若此时存在线程阻塞在该信号量时,则唤醒其中一个。

当信号量中使用布尔变量作为共享数据时,则仅允许一个线程进入临界区,实现并发互斥。通常将该信号量称为锁。

CPP中的锁

分类

在CPP中,根据不同的标准,大致可以分为如下部分:

其中,各标准的含义如下:

  1. 可重入:在一个线程获得目标锁后,再次请求同一把锁时不会阻塞。

  2. 限时性:在线程阻塞等待目标锁时,可以设置超时时间,超时时即使未获得目标锁也会解除阻塞。(不是锁的通常分类,但CPP实现中将其区分出来。)

  3. 分读写:上锁操作分为读锁和写锁,其中,写锁和互斥锁相同,一旦加上便无法再加上其他写锁和读锁;读锁则可以加上多重,即加上后可以再加上其他读锁,但无法再加上写锁。

根据上述分类标准,CPP中的锁的属性如下:

锁类型 可重入 可限时 分读写
mutex n n n
timed_mutex n y n
recursive_mutex y n n
recursive_timed_mutex y y n
shared_mutex n n y
shared_timed_mutex n y y

公共接口

所有锁都提供了一些相似的接口,例如,所有的锁只能够调用默认构造函数创建,不能够进行拷贝和移动。其他相似接口如下:

接口 含义
void lock(); 获得目标锁,若无法获得则阻塞当前线程。
如果使用的锁可重入,则同一线程可多次调用,否则会在第二次调用时发生死锁。
bool try_lock() noexcept; 尝试获得锁,若无法立即获得也会立即返回false,否则获得该锁并返回true。
void unlock(); 释放目标锁。

上述接口所有的锁都具有,因此在锁的RAII机制中具有重要作用。

限时接口

所有可限时的锁都提供了一些相似的接口,这些相似接口如下:

接口 含义
template <class Rep, class Period>
bool try_lock_for(const std::chrono::duration<Rep, Period>& timeout_duration);
尝试获得目标锁,若未能在timeout_duration的时间内获得该锁则放弃申请,返回false;否则获得该锁,返回true。
timeout_duration小于等于timeout_duration.zero()时等价于try_lock。
template <class Clock, class Duration>
bool try_lock_until(const std::chrono::time_point<Clock, Duration>& timeout_time)
尝试获得目标锁,若未能在时间timeout_time前内获得该锁则放弃申请,返回false;否则获得该锁,返回true。
timeout_time早于当前时间时等价于try_lock。

读写接口

所有分读写的锁使用lock接口作为写锁接口,都提供了一些相似的读锁接口,这些相似接口如下:

接口 含义
void lock_shared(); 获得目标读锁,若无法获得则阻塞当前线程。
bool try_lock_shared() noexcept; 尝试获得目标读锁,若无法立即获得也会立即返回false,否则获得该锁并返回true。
void unlock_shared(); 释放目标锁。

读写限时接口

所有分读写且可限时同时也提供了一些相似的可限时读锁接口,这些相似接口如下:

接口 含义
template <class Rep, class Period>
bool try_lock_shared_for(const std::chrono::duration<Rep, Period>& timeout_duration);
尝试获得目标锁,若未能在timeout_duration的时间内获得该锁则放弃申请,返回false;否则获得该锁,返回true。
timeout_duration小于等于timeout_duration.zero()时等价于try_lock_shared。
template <class Clock, class Duration>
bool try_lock_shared_until(const std::chrono::time_point<Clock, Duration>& timeout_time)
尝试获得目标锁,若未能在时间timeout_time前内获得该锁则放弃申请,返回false;否则获得该锁,返回true。
timeout_time早于当前时间时等价于try_lock_shared。

CPP在锁上的RAII机制

CPP提供了四个类用于实现基于RAII的锁机制,这些类均能够在构造函数中锁上传入的锁,在析构函数中释放对应的锁,有效的解决了因代码改动新加分支等原因导致的锁未释放问题。

同时,还允许通过传入参数进行锁定策略的修改,相应的锁定策略如下:

策略参数 策略
<默认> 调用传入参数的上锁方法,会发生阻塞。
std::defer_lock 假设当前未获得锁,之后会手动申请。
std::try_to_lock 尝试获取锁,但有可能失败,这不会阻塞。
std::adopt_lock 假设当前已经获取了锁。
std::chrono::duration 调用传入参数的lock_for方法,会发生阻塞。
std::chrono::time_point 调用传入参数的lock_until方法,会发生阻塞。

实现RAII机制的类的类型和特点如下:

类型 特点 支持策略
lock_guard 不可移动,不可拷贝 adopt_lock
scoped_lock 不可移动,不可拷贝。
能够一次申请多把锁。
adopt_lock
unique_lock 可移动,不可拷贝 defer_lock、try_to_lock、adopt_lock、duration、time_point
shared_lock 可移动,不可拷贝。
申请的是读锁。
defer_lock、try_to_lock、adopt_lock、duration、time_point

同时,unique_lockshared_lock还提供了如下接口:

接口 含义
void lock(); 获得目标锁,若无法获得则阻塞当前线程。
如果使用的锁可重入,则同一线程可多次调用,否则会在第二次调用时发生死锁。
bool try_lock() noexcept; 尝试获得锁,若无法立即获得也会立即返回false,否则获得该锁并返回true。
template <class Rep, class Period>
bool try_lock_for(const std::chrono::duration<Rep, Period>& timeout_duration);
尝试获得目标锁,若未能在timeout_duration的时间内获得该锁则放弃申请,返回false;否则获得该锁,返回true。
timeout_duration小于等于timeout_duration.zero()时等价于try_lock。
template <class Clock, class Duration>
bool try_lock_until(const std::chrono::time_point<Clock, Duration>& timeout_time)
尝试获得目标锁,若未能在时间timeout_time前内获得该锁则放弃申请,返回false;否则获得该锁,返回true。
timeout_time早于当前时间时等价于try_lock。
void unlock(); 释放目标锁。
mutex_type* release() noexcept; 未释放目标锁的放弃对目标锁的控制权。
bool owns_lock() const noexcept; 返回其是否持有已锁定的锁。
explicit operator bool() const noexcept; 隐式类型转换函数,相当于调用owns_lock。

CPP提供的通用锁定方法

CPP提供以下能够获取锁的方法:

接口 含义
template <class Lockable1, class Lockable2, class… LockableN>
void lock(Lockable1& lock1, Lockable2& lock2, LockableN&… lockn);
使用死锁避免算法尝试获得多把锁。
template <class Lockable1, class Lockable2, class… LockableN>
int try_lock(Lockable1& lock1, Lockable2& lock2, LockableN&… lockn);
从头开始锁定传入的锁,如果存在失败的,则立即返回失败锁在传入参数的以0为底的下标;若都获取成功,返回-1。

CPP条件变量

条件变量将会是CPP线程同步的重要机制,其原理和信号量类似,是通过线程的阻塞和唤醒实现的。

CPP中提供了condition_variablecondition_variable_any作为条件变量,两者完全一致,唯一的区别是condition_variablewait时只能使用std::unique<std::mutex>作为参数而condition_variable_anywait时能够使用任意锁作为参数。

两者提供了相似的接口,具体如下:

接口 含义
void notify_one() noexcept; 唤醒正在等待该条件变量的一个线程。
void notify_all() noexcept; 唤醒正在等待该条件变量的所有线程,即使被唤醒线程因为锁竞争等原因再次阻塞也不会再次等待条件变量信号。
void wait(Lock& lock); 等待条件变量唤醒信号,要求lock已经由当前线程持有。
void wait(Lock& lock, Predicate pred); 相当于while(!pred()) { wait(lock); }。
std::cv_status wait_until(Lock& lock, const std::chrono::time_point<Clock, Duration>& abs_time); 等待条件变量唤醒信号或到达指定时间,要求lock已经由当前线程持有。
bool wait_until(Lock& lock, const std::chrono::time_point<Clock, Duration>& abs_time, Predicate pred); 相当于while(!pred) { if(wait_until(lock, abs_time) == std::cv_status::timeout) { return pred(); } } return true;。
std::cv_status wait_for(Lock& lock, const std::chrono::duration<Rep, Period>& rel_time); 相当于wait_until(lock, std::chrono::steady_clock::now() + rel_time)。
bool wait_for(Lock& lock, const std::chrono::duration<Rep, Period>& rel_time, Predicate pred); 相当于wait_until(lock, std::chrono::steady_clock::now() + rel_time, std::move(pred));。

其中,std::cv_status包含以下类型:

  1. std::cv_status::no_timeout:未超时

  2. std::cv_status::timeout:超时