首页 > 其他分享 >线程间共享数据-各种锁(总结)

线程间共享数据-各种锁(总结)

时间:2023-09-21 11:36:51浏览次数:45  
标签:总结 std lock 互斥 guard mutex 线程 共享

std::mutex

#include <mutex>
#include <list>
std::mutex some_mutex;
std::list<int> mylist;

void func(int value) {
    some_mutex.lock();          // 加锁
    mylist.push_back(value);
    some_mutex.unlock();        // 解锁
}

std::lock_guard<>

类模板 std::lock_guard<> 使用了RAII技术,在构造时给互斥加锁,在析构时解锁。

#include <list>
#include <mutex>

std::list<int> mylist;
std::mutex some_mutex;

void foo(int value) {
    std::lock_guard<std::mutex> guard(some_mutex); // 自动加锁
    mylist.push_back(value);
} // 析构时解锁互斥

C++17新特性:类模板参数推导

std::lock_guard<std::mutex> guard(some_mutex); // before C++ 17
std::lock_guard guard(some_mutex); // C++ 17

std::lock()

需要锁住多个互斥时,为了防范死锁,应该始终按相同的顺序锁住互斥。std::lock() 帮我们解决了这一问题,它可以同时锁住多个互斥,而没有发生死锁的风险。

// 使用std::lock() 和 std::lock_guard<>,进行内部的数据互换操作
class some_big_object;
void swap(some_big_object& lhs, some_big_object& rhs);

class X {
private:
    some_big_object some_detail;
    std::mutex m;
public:
    X(const some_big_object &sd) : some_detail(sd) {}
    friend void swap(X& lhs, X& rhs) {
        if(&lhs == &rhs) return;
        std::lock(lhs.m, rhs.m);
        std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
        std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
        swap(lhs.some_detail, rhs.some_detail);
    }
}

std::adopt_lock(领养锁)

std::adopt_lock 指明互斥已被锁住,即互斥上有锁存在,std::lock_guard 实例应当据此接收互斥的归属权,不得在构造函数内试图另行加锁。

std::mutex m1, m2;

void func() {
    std::lock(m1, m2);
    std::lock_guard<std::mutex> lock_a(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lock_b(m2, std::adopt_lock);
}

std::scoped_lock<>

std::scoped_lock<> 是C++17引入的增强版的lock_guard,以多个互斥对象作为构造函数的参数列表,在构造时同时锁定多个互斥,在析构时同时解锁互斥。以上代码可以被简化为:

std::mutex m1, m2;

void func() {
    std::scoped_lock guard(m1, m2);
}

建议

  • 将互斥和受保护的数据组成一个类。
  • 警惕成员函数返回指针或引用,若它们指向受保护的数据,互斥会被打破。
  • 若成员函数在自身内部调用了别的函数,而这些函数却不受我们掌控,那么也不得向它们传递指针或引用。
  • 警惕接口是否存在固有的条件竞争。

防范死锁

  1. 避免嵌套锁。假如已经持有锁,就不要试图获取第二个锁。如确有需要获取多个锁,应该使用 std::lock() 或 std::scoped_lock<>,一次性获取全部锁来避免死锁。

  2. 一旦持有锁,就必须避免调用由用户提供的程序接口。

  3. 依从固定顺序获取锁。如果多个锁是绝对必要的,却无法通过 std::lock() 在一步操作中全部获取,我们只能退而求其次,在每个线程内部都依从固定顺序获取这些锁。

  4. 按层级加锁。若某线程已对低层级互斥加锁,就不准它对高层级互斥加锁。

  5. 将准则推广到锁操作之外。死锁现象并不单单因加锁操作而发生,任何同步机制导致的循环都会导致死锁的出现。

std::unique_lock<>

std::unique_lock<> 更为灵活,不一定始终占有与之关联的互斥,但性能相比 std::lock_guard 较低。其构造函数接受第二个参数:可以传入 std::adopt_lock 实例,指明 std::unique_lock 对象管理互斥上的锁;也可以传入 std::defer_lock 实例,从而使互斥在完成构造时处于无锁状态,等以后有需要时才在 std::unique_lock 对象上调用 lock() 而获取锁,或把 std::unique_lock 对象交给 std::lock() 函数加锁。std::unique_lock 也允许它的实例在被销毁前解锁,其成员函数 unlock() 负责解锁操作。

std::mutex m1, m2;
void func() {
    std::unique_lock<std::mutex> lock_a(m1, std::defer_lock);
    std::unique_lock<std::mutex> lock_b(m2, std::defer_lock); // 实例 std::defer_lock 将互斥保留为无锁状态
    std::lock(lock_a, lock_b); // 到这里才对互斥加锁
}

在不同作用域之间转移互斥归属权

std::unique_lock<std::mutex> get_lock() {
    extern std::mutex some_mutex;
    std::unique_lock<std::mutex> lk(some_mutex);
    prepare_data();
    return lk;
}

void process_data() {
    std::unique_lock<std::mutex> lk(get_lock());
    do_something();
}

std::call_once() 和 std::once_flag

令所有线程共同使用 std::call_once() 调用初始化函数,可以确保初始化由其中某线程安全且唯一地完成。必要的同步数据由 std::once_flag 实例存储,每个 std::once_flag 实例对应一次不同的初始化。std::once_flag 实例既不可复制也不可移动。

实现延迟初始化

std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag;

void init_resource() {
    resource_ptr.reset(new some_resource);
}

void foo() {
    std::call_once(resource_flag, init_resource);   // 初始化函数被准确地唯一一次调用
    resource_ptr->do_something();
}

// 利用 std::call_once() 函数对类 X 的数据成员实施线程安全的延迟初始化
class X {
private:
    connection_info connection_details;
    connection_handle connection;
    std::once_flag connection_init_flag;

    void open_connection() {
        connection = connection_manager.open(connection_details);
    }

public:
    X(const connection_info& connection_details_):
        connection_details(connection_details_) {}
    
    void send_data(const data_packet& data) {
        std::call_once(connection_init_flag, &X::open_connection, this);
        connection.send_data(data);
    }

    data_packet receive_data() {
        std::call_once(connection_init_flag, &X::open_connection, this);
        return connection.receive_data();
    }

};

C++标准规定,只要控制流第一次遇到静态数据的声明语句,变量即进行初始化。C++11规定初始化只会在某一线程上单独发生。某些类的代码只需要用到唯一一个全局实例,这种情形可用以下方法代替 std::call_once:

class my_class;
my_class& get_my_class_instance() {
    static my_class instance;
    return instance;
}

读写互斥

允许单独一个 “写线程” 进行完全排他的访问,也允许多个 “读线程” 共享数据或并发访问。

排他锁(写锁)

std::lock_guard<std::shared_mutex>
std::unique_lock<std::shared_mutex>

共享锁(读锁)

std::shared_lock<std::shared_mutex>

多个线程能够同时锁住同一个 std::shared_mutex。若共享锁已被某些线程所持有,若别的线程试图获取排他锁,就会发生阻塞,直到那些线程全部都释放该共享锁。反之,若任一线程持有排他锁,那么其他线程全部无法获取共享锁或排他锁,直到持锁线程将排他锁释放为止。

// 运用 std::shared_mutex 保护数据结构,以简易的 DNS 缓存表为例
#include <map>
#include <string>
#include <mutex>
#include <shared_mutex>

class dns_entry;
class dns_cache {
private:
    std::map<std::string, dns_entry> entries;
    mutable std::shared_mutex entry_mutex;
public:
    dns_entry find_entry(const std::string& domain) const { // 多线程可以同时调用
        std::shared_lock<std::shared_mutex> lk(entry_mutex);    // 共享锁
        const std::map<std::string, dns_entry>::const_iterator it = entries.find(domain);
        return (it == entries.end() ? dns_entry() : it->second);
    }

    void update_or_add_entry(const std::string& domain, const dns_entry& dns_details) { // 多线程进行排他访问
        std::lock_guard<std::shared_mutex> lk(entry_mutex); // 排他锁
        entries[domain] = dns_details;    
    }
};

共享锁,也叫读写锁,主要应用与读多写少的场景。

比如,在多线程环境下,多个线程操作同一个文件,其中读文件的操作比写文件的操作更加频繁,那么在进行读操作时,不需要互斥,线程间可以共享这些数据,随意的读取。但是一旦有写操作,那么一定要进行互斥操作,否则读取到的数据可能存在不一致。

C++14 共享超时互斥锁 shared_timed_mutex

读线程 调用 lock_shared()获取共享锁,写线程 调用 lock() 获取互斥锁。

  • 当调用lock()的时候,如果有线程获取了共享锁,那么写线程会等待,直到所有线程将数据读取完成释放共享锁,再去锁定资源,进行修改;
  • 当调用lock_shared()时,如果有写线程获取了互斥锁,那么需要等待
  • 当调用lock_shared()时,如果有读线程获取共享锁,也会直接返回,获取成功

 

递归锁

MutexLock mutex;  
 
void foo()  
{  
    mutex.lock();  
    // do something  
    mutex.unlock();  
}  
 
void bar()  
{  
    mutex.lock();  
    // do something  
    foo();  
    mutex.unlock();   
}

foo函数和bar函数都获取了同一个锁,而bar函数又会调用foo函数。如果MutexLock锁是个非递归锁,则这个程序会立即死锁。因此在为一段程序加锁时要格外小心,否则很容易因为这种调用关系而造成死锁。
不要存在侥幸心理,觉得这种情况是很少出现的。当代码复杂到一定程度,被多个人维护,调用关系错综复杂时,程序中很容易犯这样的错误。庆幸的是,这种原因造成的死锁很容易被排除。
但是这并不意味着应该用递归锁去代替非递归锁。递归锁用起来固然简单,但往往会隐藏某些代码问题。比如调用函数和被调用函数以为自己拿到了锁,都在修改同一个对象,这时就很容易出现问题。因此在能使用非递归锁的情况下,应该尽量使用非递归锁,因为死锁相对来说,更容易通过调试发现。程序设计如果有问题,应该暴露的越早越好。

 

问题就是:加锁的操作需要相互嵌套,如果使用std::mutex 肯定会导致死锁,而重构代码,提取出共用部分的工作量又很大。

这个时候我发现了好东西 std::recursive_mutex 递归锁

递归锁可以允许一个线程对同一互斥量多次加锁,解锁时,需要调用与lock()相同次数的unlock()才能释放使用权

这边再介绍一个好东西:

std::lock_guard<std::recursive_mutex> 

std::lock_guard在构造函数中加锁,在析构函数中解锁,利用这个类可以减少我们对加锁可解锁操作的管理工作,专注于逻辑实现。

标签:总结,std,lock,互斥,guard,mutex,线程,共享
From: https://www.cnblogs.com/bwbfight/p/17719483.html

相关文章

  • 数组变异方法和非变异方法的总结
    区别:1.操作数组的方法中,分为变异方法和非变异方法。2.其中,变异方法意味着会改变原数组,而非变异方法则只会返回一个新数组,不会修改原始数组数组变异方法:push()//数组尾部追加一个元素pop()//数组尾部弹出一个元素shift()//数组头部弹出一个元素unshift()//数组头部插入一个......
  • 在 Python 中,可以使用线程池(ThreadPoolExecutor)和 wait 方法来等待线程池中的所有任务
    importconcurrent.futures#创建一个线程池withconcurrent.futures.ThreadPoolExecutor()asexecutor:#提交任务给线程池task1=executor.submit(func1,arg1)task2=executor.submit(func2,arg2)task3=executor.submit(func3,arg3)#使......
  • Python-多线程调用计算请求时间
    使用多线程调用某个方法(请求),计算每个线程消耗时间importthreadingimporttimeimportrequestsimportjsonimportconcurrent.futuresdefinput_req():url="https://xxxxxxxxxxxx"approval_content="nullain"payload=json.dumps({&quo......
  • Redis漏洞总结--未授权--沙箱绕过--(CNVD-2015-07557)&&(CNVD-2019-21763)&&(CVE-2022
    Redis未授权--沙箱绕过--(CNVD-2015-07557)&&(CNVD-2019-21763)&&(CVE-2022-0543)环境复现采用Vulfocus靶场进行环境复现,官网docker搭建有问题,具体搭建教程参考vulfocus不能同步的解决方法/vulfocus同步失败CNVD-2015-07557未授权访问影响版本Redis<=5.0.5漏洞探测使用端......
  • 每日总结
    今日收获学会了有关hive的数据清洗;也算是重温了有关hive的相关知识;背了单词!!做了几道软考的题目,嗯,选择题好难;明天预计继续我的背单词旅程;继续学习hive的相关知识去!软考难,还是得学!!和友友一起报名了程序设计大赛,加油加油!!!还报名了英语六级、软考、网络安全知识竞赛...好忙......
  • 今日总结
    今天学习了hadoop伪分布式3.2伪分布式运行模式3.2.1启动HDFS并运行MapReduce程序配置集群,修改Hadoop的配置文件(/hadoop/hadoop-2.7.7/etc/hadoop目录下)①core-site.xml<configuration><!--指定HDFS中NameNode的地址--><property><name>fs.defaultFS</name>......
  • 每日总结
    今天又测试了一下hbase的安装hbase的启动没有任何问题但是phoenix的安装还是不行,已经按照步骤一步步的安装但是每次到启动的前一句就会卡住着了各种各样的解决方法还是不行,有同学问题和我一样但是用他的就能成功但是我的就不行,莫名其妙哦。然后今天复习了软考的基础知识,将前面......
  • 2023.9.20——每日总结
    学习所花时间(包括上课):9h代码量(行):0行博客量(篇):1篇今天,上午上课,下午做任务。我了解到的知识点:1.了解了关于模型训练的一些知识和注意事项;2.了解了关于软件构造的一些知识,明日计划:1.上课;2.比赛;......
  • 线程池配置
    @Configuration@EnableAsyncpublicclassExecutorConfig{privatestaticfinalintpoolSizeMin=30;@BeanpublicExecutorexecutor(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();StringpoolSize="60......
  • 每日总结(侧边栏初步设置)
     1<template>2<!--菜单栏-->3<!--default-openeds参数为默认展开:default-openeds="['1','2']"-->4<el-menu:default-openeds="['']"5style="min-height:100......