谈到多线程编程,我们必须了解一下C++中提供的一些保证数据的同步和互斥的锁机制。
必谈的RAII原则
RAII原则是所有的资源都必须有管理对象,而资源的申请操作在管理对象的构造函数中进行,而资源的回收则在管理对象的析构函数中进行。
先来讲讲std::mutex
std::mutex是C++标准库中提供的最基本的互斥锁之一。它用于实现线程间的互斥访问,即在一个时间点只允许一个线程获得锁,其他线程需要等待锁被释放才能继续执行。使用std::mutex可以保证多个线程对共享资源的访问顺序,并避免数据竞争产生的问题。
在应用过程中,每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。通过“锁”就将资源的访问变成互斥操作,之后与时间有关的错误也不会再产生。但是同一时刻,只能有一个线程持有该锁。当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不加锁,直接访问该全局变量,依然能够访问,但会出现数据混乱。所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但并没有强制限定。
主要应用函数:(成功返回0, 失败返回错误号)
- 初始化一个互斥锁,参数1为传出参数(调用时应传 &mutex),参数二是互斥量属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)。
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);</pre>
<pre>pthread_mutex_destroy()----销毁一个互斥锁
pthread_mutex_lock()----加锁
pthread_mutex_trylock()----尝试加锁
pthread_mutex_unlock()----解锁
注意:互斥锁初始化有两种方式:
- 静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。例如:
pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
- 动态初始化:局部变量应采用动态初始化。例如:
pthread_mutex_init(&mutex, NULL)
加锁与解锁:
- lock与unlock以及trylock:lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。但是,trylock加锁失败直接返回错误号(如:EBUSY),不阻塞。
需要注意的是:
std::mutex.lock是我们在C++中比较常见的锁,我们使用std::mutex.lock方法时,同时需要考虑何时使用std:mutex.unlock方法去解锁。如果在复杂的多线程情况下,加锁、解锁的时机很难把握,也不好实现。所以C++在新标准里提供了lock_guard, unique_lock, shared_lock, 和 scoped_lock四种锁,用于各种复杂情况。这四种锁都是满足RAII风格。
lock_guard
lock_guard用于在作用域内自动管理互斥量的锁定和解锁。当 lock_guard 对象被创建时,它会自动锁定互斥量,当对象离开作用域时,它会自动解锁互斥量。lock_guard 不支持手动锁定和解锁,也不支持条件变量,基本用法如下:
std::lock_guard<std::mutex> lock(mtx);
unique_lock
它提供了比std::lock_guard更多的功能和灵活性,但是在不需要额外灵活性时,使用后者可能是一个更高效的选择。
- 默认构造
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
若仅传递一个互斥量给 std::unique_lock 的构造函数,默认会立即尝试上锁
- 延迟锁定
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// 此时 mtx 没有被锁定,可以稍后通过调用 lock.lock() 来手动上锁
- 尝试锁定-创建 std::unique_lock 对象时尝试锁定互斥量,如果互斥量已被其他线程锁定,也不会阻塞当前线程。
std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
- adopt_lock --如果互斥量已经被手动锁定,我们可以使用 std::unique_lock 并传递 std::adopt_lock 标记来接管互斥量的所有权,这告诉 std::unique_lock 对象互斥量已经被锁定。
std::mutex mtx;
mtx.lock();//--切记手动锁定
std::unique_lock<std::mutex> lock(mtx, std::adopt_lock);
// 此时 mtx 被认为已经被 lock 锁定
- 释放锁 unlock()
如果 std::unique_lock 对象当前持有锁(即拥有锁的标记为 true),则调用此方法会释放互斥量,并将拥有锁的标记设置为 false。如果对象没有持有锁,则调用 unlock() 会导致程序行为未定义。
shared_lock
shared_lock:用于共享互斥量(std::shared_mutex 或 std::shared_timed_mutex)的锁,允许多个线程同时读取共享数据,但在写入数据时仍然保证互斥。当对象离开作用域时,它会自动解锁共享互斥量。但是其支持手动锁定和解锁,以及尝试锁定。
scoped_lock
scoped_lock用于同时锁定多个互斥量,以避免死锁。scoped_lock 是一个 RAII 风格的锁,当对象离开作用域时,它会自动解锁所有互斥量。不支持手动锁定和解锁,也不支持条件变量。它的主要用途是在需要同时锁定多个互斥量时提供简单且安全的解决方案。
有关于相关例子,可以参考文章[例子]。