C++11的互斥包装器

2023-11-17 16:23:09 浏览数 (2)

1. 为何要引入互斥包装器?

在C 多线程中会经常用到mutex,在使用的时候lock后,有时候会忘记使用unlock进行解锁造成死锁,或者在lockunlock之间代码异常跳出,导致程序无法执行到unlock造成死锁,因此在C 11中引入互斥体包装器,互斥体包装器为互斥提供了便利的RAII风格机制,本质上就是在包装器的构造函数中加锁,在析构函数中解锁,将加锁和解锁操作与对象的生存期深度绑定,防止使用mutex加锁(lock)后,忘记解锁(unlock)或者两者之间出现异常退出等造成死锁。

RAII(Resource Acquisition Is Initialization, 资源获取即初始化) RAII是一种 C 编程技术 ,它将必须在使用前请求的资源(分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥体、磁盘空间、数据库连接等——任何存在受限供给中的事物)的生命周期与一个对象的生存期相绑定。RAII 保证资源能够用于任何会访问该对象的函数(资源可用性是一种类不变式,这会消除冗余的运行时测试)。它也保证对象在自己生存期结束时会以获取顺序的逆序释放它控制的所有资源。

C 11提供了lock_guardunique_lock两种互斥包装器。

2. lock_guard

lock_guard 是互斥体包装器,为在作用域块期间占有互斥提供便利RAII风格机制。其在头文件<mutex>中定义,其函数原型如下:

代码语言:javascript复制
template< class Mutex >
class lock_guard;

其构造函数如下:

代码语言:javascript复制
//等效地调用 m.lock() 
explicit lock_guard( mutex_type& m );  //C  11 起

//获得互斥 m 的所有权而不试图锁定它。若当前线程不在 m 上保有非共享锁
//(即由 lock、 try_lock、 try_lock_for 或 try_lock_until 
//取得的锁)则行为未定义。
lock_guard( mutex_type& m, std::adopt_lock_t t ); //C  11 起

//复制构造函数被删除
lock_guard( const lock_guard& ) = delete; //C  11 起

析构函数如下:

代码语言:javascript复制
//释放所占有互斥的所有权。
//等效地调用 m.unlock() ,
//其中 m 是传递个 lock_guard 的构造函数的互斥
~lock_guard(); //C  11 起

创建 lock_guard 对象时,它试图接收给定互斥的所有权。控制离开创建 lock_guard 对象的作用域时,销毁 lock_guard 并释放互斥。lock_guard 类不可复制。

:若 m 先于 lock_guard 对象被销毁,则行为未定义。

示例:

代码语言:javascript复制
#include <thread>
#include <mutex>
#include <iostream>
 
int g_i = 0;
std::mutex g_i_mutex;  // 保护 g_i
 
void safe_increment()
{
    std::lock_guard<std::mutex> lock(g_i_mutex);
      g_i;
 
    std::cout << std::this_thread::get_id() << ": " << g_i << 'n';
 
    // g_i_mutex 在锁离开作用域时自动释放
}
 
int main()
{
    std::cout << "main: " << g_i << 'n';
 
    std::thread t1(safe_increment);
    std::thread t2(safe_increment);
 
    t1.join();
    t2.join();
 
    std::cout << "main: " << g_i << 'n';
}

可能的输出:

代码语言:javascript复制
main: 0
140641306900224: 1
140641298507520: 2
main: 2

3. unique_lock

unique_lock也是C 11提供的一种通用互斥包装器,它允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。其也在头文件<mutex>中定义,其构造函数如下:

代码语言:javascript复制
//构造无关联互斥的 unique_lock 
unique_lock() noexcept; //C  11 起

//移动构造函数。以 other 的内容初始化 unique_lock 。令 other 无关联互斥
unique_lock( unique_lock&& other ) noexcept; //C  11 起

// 构造以 m 为关联互斥的 unique_lock
// 通过调用 m.lock() 锁定关联互斥
explicit unique_lock( mutex_type& m ); //C  11 起

// 构造以 m 为关联互斥的 unique_lock
// 不锁定关联互斥
unique_lock( mutex_type& m, std::defer_lock_t t ) noexcept; //C  11 起

// 构造以 m 为关联互斥的 unique_lock
// 通过调用 m.try_lock() 尝试锁定关联互斥而不阻塞。
// 若 Mutex 不满足可锁定 (Lockable) 则行为未定义
unique_lock( mutex_type& m, std::try_to_lock_t t ); //C  11 起

// 构造以 m 为关联互斥的 unique_lock 
// 假定调用方线程已保有 m 上的非共享锁(即由 lock、 try_lock、 try_lock_for 
// 或 try_lock_until 取得的锁)。若非如此则行为未定义
unique_lock( mutex_type& m, std::adopt_lock_t t ); //C  11 起

// 构造以 m 为关联互斥的 unique_lock 
// 通过调用 m.try_lock_for(timeout_duration) 尝试锁定关联互斥。
// 阻塞到经过指定的 timeout_duration 或获得锁这两个事件的先到来者为止
template< class Rep, class Period >
unique_lock( mutex_type& m,
             const std::chrono::duration<Rep,Period>& timeout_duration ); //C  11 起

// 构造以 m 为关联互斥的 unique_lock
// 通过调用 m.try_lock_until(timeout_time) 尝试锁定关联互斥。
// 阻塞到抵达指定的 timeout_time 或获得锁这两个事件的先到来者为止
template< class Clock, class Duration >
unique_lock( mutex_type& m,
             const std::chrono::time_point<Clock,Duration>& timeout_time ); //C  11 起

unique_lock除了提供 lock_guard有的基础功能外,还提供了锁定等相关的方法,使得其更加灵活方便,其提供的方法有:

函数

说明

备注

lock

锁定关联互斥

公开成员函数

try_lock

尝试锁定关联互斥,若互斥不可用则返回

公开成员函数

try_lock_for

试图锁定关联的定时可锁互斥,若互斥在给定时长中不可用则返回

公开成员函数

try_lock_until

尝试锁定关联可定时锁互斥,若抵达指定时间点互斥仍不可用则返回

公开成员函数

unlock

解锁关联互斥

公开成员函数

swap

与另一std::unique_lock 交换状态

公开成员函数

release

将关联互斥解关联而不解锁它

公开成员函数

mutex

返回指向关联互斥的指针

公开成员函数

own_lock

测试锁是否占有其关联互斥

公开成员函数

operator bool

测试锁是否占有其关联互斥

公开成员函数

std::swap

std::swap对 unique_lock 的特化,功能与其成员函数swap类似

非成员函数

示例:

代码语言:javascript复制
#include <mutex>
#include <thread>
#include <iostream>
#include <vector>
#include <chrono>
 
int main()
{
    int counter = 0;
    std::mutex counter_mutex;
    std::vector<std::thread> threads;
 
    auto worker_task = [&](int id) {
        std::unique_lock<std::mutex> lock(counter_mutex);
          counter;
        std::cout << id << ", initial counter: " << counter << 'n';
        lock.unlock();
 
        // 我们模拟昂贵操作时不保有锁
        std::this_thread::sleep_for(std::chrono::seconds(1));
 
        lock.lock();
          counter;
        std::cout << id << ", final counter: " << counter << 'n';
    };
 
    for (int i = 0; i < 10;   i) threads.emplace_back(worker_task, i);
 
    for (auto &thread : threads) thread.join();
}

可能的输出:

代码语言:javascript复制
0, initial counter: 1
1, initial counter: 2
2, initial counter: 3
3, initial counter: 4
4, initial counter: 5
5, initial counter: 6
6, initial counter: 7
7, initial counter: 8
8, initial counter: 9
9, initial counter: 10
6, final counter: 11
3, final counter: 12
4, final counter: 13
2, final counter: 14
5, final counter: 15
0, final counter: 16
1, final counter: 17
7, final counter: 18
9, final counter: 19
8, final counter: 20

4.两者之间的不同

lock_guard的使用方法非常简单,通过构造函数上锁,在销毁的时候解锁,对于一些简单的场景使用也非常方便高效,但对于一些作用域比较大的场景,可能会影响效率,例如如下场景:

代码语言:javascript复制
int g_i = 0;
std::mutex g_i_mutex;  // 保护 g_i

void safe_increment()
{
    std::lock_guard<std::mutex> lock(g_i_mutex);
      g_i;
 
    std::cout << std::this_thread::get_id() << ": " << g_i << 'n';

    //流程1开始
    ...
    //流程1结束
 
    // g_i_mutex 在锁离开作用域时自动释放
}

如上例所述,如果流程1的过程特别长,而且不涉及g_i的操作,如果使用lock_guard的话会导致g_i上锁时间特别长,影响其他线程的对其所有权的获取,影响整个代码的运行效率。因此,针对这种应用场景,我们应该使用unique_lockg_i进行互斥锁管理,我们可以在流程1的开始处,进行手动解锁,提前释放g_i的所有权,提高程序的效率。

代码语言:javascript复制
int g_i = 0;
std::mutex g_i_mutex;  // 保护 g_i

void safe_increment()
{
    std::unique_lock<std::mutex> lock(g_i_mutex);
      g_i;
 
    std::cout << std::this_thread::get_id() << ": " << g_i << 'n';
    lock.unlock(); //提前释放g_i的所有权
    //流程处理1开始
    ....
    //流程处理1结束
 
    // g_i_mutex 在锁离开作用域时检测到已经unlock了,就不会再次调用unlock
}

:对于上面的例子,lock_guard也可以通过{ }来控制lock_guard对象的作用域,进而将控锁的范围进一步缩小。

unique_lock除了提供可以手动解锁的方法外,还额外提供了try_lock_fortry_lock_until等带时间的加锁方法,以及其他的特殊方法,我们可以根据不同的应用场景选择合适的方法。

5. 总结

unique_locklock_guard最大的区别在于unique_lock提供了手动解锁的方法,增加了中途解锁的功能,而不是像lock_guard必须等待对象析构时解锁,增加了控锁数据的精细程度,提高程序的效率。

同时unique_lock还提供了更多的公有方法供我们按需使用。但是,方便肯定是有代价的,unique_lock在增加这些新方法的同时,方法内部也增加一些新的逻辑和资源占用,例如unlock功能,其内部需要维护一个锁的状态,所以整体在效率上会比lock_guard差一点。因此对于普通的简单场景,lock_guard也是不错的选择。

0 人点赞