跟我学C++中级篇——std::unique_lock的分析应用

一、C++中的锁管理

在多线程的编程中,一般都是使用锁或类似的机制。简单的说,对一个锁变量,一般要进行lock和unlock的动作。前者用来锁住并发的资源,待占用此资源的线程使用完成后再使用unlock的动作,好让此资源可以被其它的线程继续使用。但在这个过程中,不少的开发者往往在业务逻辑的控制中,lock和unlock没有达成配对使用或锁住的资源粒度太大,导致线程的死锁或大量的线程未被完全利用。
针对这种情况,C++中可以使用RAII技术,来处理锁的自动控制。在C++11后,STL提供了lock_guard,scoped_lock,shared_lock以及unique_lock等RAII封装接口。当然,为了更好的对锁进行控制,STL中提供的接口更加丰富,如try_lock等。本文将针对其中的unique_lock进行具体的应用分析。

二、std::unique_lock

在标准库的锁控制接口中,std::unique_lock是一个广泛应用的类。它与lock_guard相比,提供了更高级的应用,让开发者对锁的控制更精细。做为一个通用的互斥体RAII封装类,它支持延迟加锁、定时加锁、递归锁定和锁的所有权转移。另外,它可以与条件变量一起使用,这个的应用在前面仔细分析过,开发者一定要注意它的实际应用方法,不要犯一些低级的错误。
需要向开发者说明的是std::unique_lock是可移动的但不可复制的,所以在实际应用时,一定要与实际场景契合。它核心的操作目标就是Mutex,这个Mutex不光包括STL库的,在某些情况下,只要符合这种机制的互斥体都可以使用。

三、主要的用法

std::unique_lock的用法非常灵活,主要包括以下几种:

  1. 延迟锁定
    std::defer_lock,可以设置当前的锁为延迟加锁。所谓的延迟加锁,就是在需要使用锁的范围内再lock相关的资源,而不是生成锁对象时就锁定相关资源(这也是与lock_guard的重要不同)。具体的用法如下:
#include <iostream>
#include <mutex>
#include <thread>
 
struct Box
{
    explicit Box(int num) : num_things{num} {}
 
    int num_things;
    std::mutex m;
};
 
void transfer(Box& from, Box& to, int num)
{
    // don't actually take the locks yet
    std::unique_lock lock1{from.m, std::defer_lock};
    std::unique_lock lock2{to.m, std::defer_lock};
 
    // lock both unique_locks without deadlock
    std::lock(lock1, lock2);
 
    from.num_things -= num;
    to.num_things += num;
 
    // “from.m” and “to.m” mutexes unlocked in unique_lock dtors
}
 
int main()
{
    Box acc1{100};
    Box acc2{50};
 
    std::thread t1{transfer, std::ref(acc1), std::ref(acc2), 10};
    std::thread t2{transfer, std::ref(acc2), std::ref(acc1), 5};
 
    t1.join();
    t2.join();
 
    std::cout << "acc1: " << acc1.num_things << "\n"
                 "acc2: " << acc2.num_things << '\n';
}

这种方式一般可以用在对锁粒度最小的控制的情况下应用。

  1. 尝试加锁
    大多数的情况下,锁的目的就是为锁住资源,但有些情况下,可能锁已经被使用,而此时需要控制在其它线程中是否可以再次锁定的状态,而且不能因此造成调用的阻塞。这时就可以尝试去加锁。假如锁已经被释放,则可以锁住,否则就不会锁住,抛出std::system_error。
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
 
using namespace std::chrono_literals;
 
int main()
{
    std::mutex counter_mutex;
    std::vector<std::thread> threads;
    using Id = int;
 
    auto worker_task = [&](Id id, std::chrono::seconds wait, std::chrono::seconds acquire)
    {
        // wait for a few seconds before acquiring lock.
        std::this_thread::sleep_for(wait);
 
        std::unique_lock<std::mutex> lock(counter_mutex, std::defer_lock);
        if (lock.try_lock())
            std::cout << '#' << id << ", lock acquired.\n";
        else
        {
            std::cout << '#' << id << ", failed acquiring lock.\n";
            return;
        }
 
        // keep the lock for a while.
        std::this_thread::sleep_for(acquire);
 
        std::cout << '#' << id << ", releasing lock (via destructor).\n";
    };
 
    threads.emplace_back(worker_task, Id{0}, 0s, 2s);
    threads.emplace_back(worker_task, Id{1}, 1s, 0s);
    threads.emplace_back(worker_task, Id{2}, 3s, 0s);
 
    for (auto& thread : threads)
        thread.join();
}

类似的接口在STL还提供了try_lock_for和try_lock_until两种,主要用来对超时的锁定进行控制。

  1. 移动所有权
    这个就比较简单了,也好理解,std::unique_lock是独占的,不允许直接充当右值一样赋值应用。
#include <mutex>
std::mutex mtx;
int main() {
  std::unique_lock<std::mutex> lock(mtx);
  std::unique_lock<std::mutex> l = lock;//error,must use:std::move(lock);
  return 1;
}
  1. 与条件变量一起使用
    这个在多线程中反复分析过,下面只给出简单的应用:
......
std::unique_lock<std::mutex> lock(mutex_);
        
cond_.wait(lock, [this]() {
   ......
});
        
std::string msg = std::move(messages_.front());
......

有兴趣的可以查看一下相关具体应用的例程,也可以翻看前面的多线程中相关的具体应用的例程

  1. 递归锁
#include <iostream>
class RecursiveDemo {
private:
  std::recursive_mutex rMutex_;
  int data_ = 0;

public:
  void fnu1() {
    std::unique_lock<std::recursive_mutex> lock(rMutex_);
    data_++;
    std::cout << "fun1 working!data_:" << data_ << std::endl;
    fun2();

    std::cout << "fun1 work end!data_:" << data_ << std::endl;
  }

  void fun2() {
    std::unique_lock<std::recursive_mutex> lock(rMutex_);
    data_ *= 2;
    std::cout << "fun2 work end! data_:" << data_ << std::endl;
  }
};
int main() {
  RecursiveDemo demo;
  demo.fnu1();
  return 0;
}

普通的mutex重复锁会出现问题,而使用C++ STL中提供的recursive_mutex就没有问题了。

  1. 手动管理
    手动处理比较简单,但应用时要注意锁资源的锁与释放的状态控制,不要产生死锁。
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
 
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();
 
        // don't hold the lock while we simulate an expensive operation
        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();
}

代码主要是用来显示的指定锁定和解锁的方式。
另外,在std::unique_lock的构造参数中除了defer_lock还有std::try_to_lock, std::adopt_lock两个参数,它们两个应用也很简单,前者用来表明构造对象时尝试加锁但不阻塞,可配合 owns_lock()来确定是否成功;而adopt_lock就是把已经锁住的mutex进行传参构造unique_lock,不过需要小心操作不当会产生二次释放锁的窘境。

说明:以上代码主要来自cppreference的文档。

四、总结

整体上来看,std::unique_lock比std::lock_guard在性能上要略差一些,毕竟其中增加了不少的扩展功能(如延迟加锁等)。所以std::lock_guard更适合一些简单的应用场景,而std::unique_lock多用于复杂的情况下。特别是有一些特定的应用如延迟、与条件变量配合等下,必须使用std::unique_lock。
其实很容易明白,如果能使用一种锁解决问题,标准库中也就不会出现这么多的锁的封装类。而且在后续的版本中,这种情况仍然在扩展,就是这个意思。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值