C++中多线程间共享数据详解

 更新时间:2024年01月09日 11:03:21   作者:wingのpeterPen  
这篇文章主要为大家详细介绍了C++中多线程间共享数据的相关知识,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以跟随小编一起学习一下

在 C++ 中,我们可以通过构造 std::mutex (mutual exclusion)的实例来创建互斥,调用成员函数 lock() 对其加锁,调用 unlock() 解锁。但是不推荐直接调用成员函数的做法,因为这样做,那我们就必须在该函数的每条路径上都调用 unlock(),包括异常导致退出的路径。取而代之,C++标准库提供了类模板 std::lock_guard<>,针对互斥类融合RAII手法:在构造的时候给互斥加锁,在析构时进行解锁,从而保证互斥总被正确解锁。

std::mutex的基本用法

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

void add_to_list(int new_value){
    std::lock_guard<std::mutex> guard(some_mutex);
	some_list.push_back(new_value);
}

bool list_contains(int value_to_find){
    std::lock_guard<std::mutex> guard(some_mutex);
    return std::find(some_list.begin(), some_list.end(), value_to_find) != some_list.end();
}

防范死锁

防范死锁的通常建议是,始终按相同顺序对两个互斥加锁。若我们总是先锁互斥A,再锁互斥B,则永远不会发生死锁。但是有时会出现一些棘手的问题,例如,一个函数,其操作同一个类的两个实例,交换它们内部的数据为了保证并发时,免受其他改动的影响,需要对它们进行加锁,但是函数入参的顺序可以改变,即可以是swap(A,B)也可以是swap(B,A),当同时发生这种情况时,死锁也会发生。因此需要一种方式来,来同时锁住两个互斥,而不是谁先谁后。即要求“全员共同成败”(all-or-nothing,或全部成功锁定,或没获取任何锁并抛出异常)的语义。

使用 std::lock() 函数,同时锁住互斥。

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 lock_a(lhs.m, std::adopt_lock);
        std::lock_guard lock_b(rhs.m, std::adopt_lock);

        swap(lhs.some_detail, rhs.some_detail);
    }
}

std::lock() 是一个函数,并不像 RAII 类一样,在析构时进行解锁。因此需要借助 std::lock_guard 类型的对象,让 std::mutex 在函数完成后进行解锁。std::adopt_lock 作为函数参数,表示 std::mutex 已经被锁住了,让std::lock_guard 类不需要在构造函数中对 std::mutex 进行加锁。C++ 17出现了 std::scoped_lock<> 模板类,其和 std::lock_guard<>完全等价,只不过前者是可变参数模板,接受各种互斥型别作为模板参数,还以多个互斥对象作为构造函数的参数列表,除了这些其还有与 std::lock() 函数一样的功能,因此可以改善上述代码。

void swap(X& lhs, X& rhs){
    if(&lhs == &rhs){
            return;
    }

    std::scoped_lock guard(lhs.m, rhs.m);
    swap(lhs.some_detail, rhs.some_detail);
}

std::lock_guard 和 std::unique_lock

std::unique_lock 类支持在构造时暂时不获得锁,在需要的时候手动调用 lock(),而获得锁。其含有一个内部标志 __owns__(可以通过其成员函数 owns_lock() 获得),表明关联的互斥目前是否正被该类的实例上锁。假如 std::unique_lock 实例关联的互斥的确上锁了,则其析构函数必须调用unlock();若不然,实例并未将关联的互斥上锁,便绝不能调用 unlock()。

初始化时保护共享数据

void undefined_behaviour_with_double_checked_locking()
{
    if(!resource_ptr){ //①
        std::lock_guard<std::mutex> lk(resource_mutex); // ②
        if(!resource_ptr){
            resource_ptr.reset(new some_resource);
        }
    }
    resource_ptr->do_something();
}

在双重检查锁定模式中,一号线程执行到①发现条件满足(resource_ptr 为空),同时二号线程执行到①也发现条件满足,然后继续执行上锁操作②,此时一号线程也会去执行上锁操作,但是resource_mutex上的锁已经被二号线程持有了,这样就会发生数据竞争(当然会发生恶性数据竞争的路径不止这一条)。

为了解决这种情况,C++标准库中提供了 std::onec_flag 类 和 std::call_once 类。

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();
}

保护读多写少的数据

C++ 17 标准库提供了两种新的互斥:std::shared_mutex 和 std::shared_timed_mutex(C++14就有了)。两者的区别在于后者支持更多操作。

std::shared_mutex mutex;

// 获取读锁(又叫共享锁)
std::shared_lock read_lock(mutex);

// 获取写锁(又叫排它锁)
std::lock_guard write_lock(mutex);

假设共享锁已经被某些线程持有,若别的线程试图获取排他锁,就会发生阻塞,直到这些线程全都释放该共享锁。反之,如果任一线程持有排他锁,那么其他线程都无法获取共享锁或排它锁,直到持锁线程将排它锁释放为止。

到此这篇关于C++中多线程间共享数据详解的文章就介绍到这了,更多相关C++共享数据内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 如何使用C++结合OpenCV进行图像处理与分类

    如何使用C++结合OpenCV进行图像处理与分类

    在计算机视觉领域,OpenCV与C++结合能高效处理和分类图像,C++的高执行效率适合大规模数据处理,OpenCV提供丰富的功能,如图像预处理和机器学习算法,安装OpenCV需要配置环境和添加库文件,本文详细介绍了使用C++和OpenCV进行图像分类的过程,包括使用SVM和深度学习模型
    2024-09-09
  • C/C++ Qt QThread线程组件的具体使用

    C/C++ Qt QThread线程组件的具体使用

    QThread库是QT中提供的跨平台多线程实现方案,本文详细的介绍了Qt QThread线程组件的具体使用,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-11-11
  • C++实现邻接表顶点的删除

    C++实现邻接表顶点的删除

    这篇文章主要为大家详细介绍了C++实现邻接表顶点的删除,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-04-04
  • C++用read()和write()读写二进制文件的超详细教程

    C++用read()和write()读写二进制文件的超详细教程

    二进制的文件肉眼我们是读不懂的,如果通过二进制的读写操作就可以读懂,下面这篇文章主要给大家介绍了关于C++用read()和write()读写二进制文件的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-06-06
  • C语言fprintf()函数和fscanf()函数的具体使用

    C语言fprintf()函数和fscanf()函数的具体使用

    本文主要介绍了C语言fprintf()函数和fscanf()函数的具体使用,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-11-11
  • 详细分析C++ 多态和虚函数

    详细分析C++ 多态和虚函数

    这篇文章主要介绍了C++ 多态和虚函数的相关资料,文中示例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-07-07
  • VS2017+Qt5+Opencv3.4调用摄像头拍照并存储

    VS2017+Qt5+Opencv3.4调用摄像头拍照并存储

    本文主要介绍了VS2017+Qt5+Opencv3.4调用摄像头拍照并存储,实现了视频,拍照,保存这三个功能。具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-05-05
  • OpenCV实现轮廓检测与绘制

    OpenCV实现轮廓检测与绘制

    这篇文章主要为大家详细介绍了OpenCV实现轮廓检测与绘制,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-06-06
  • Qt界面美化之自定义qss样式表的详细步骤

    Qt界面美化之自定义qss样式表的详细步骤

    很多人应该和我一样,想做界面才接触的Qt,结果就是做不出来华丽的界面,下面这篇文章主要给大家介绍了关于Qt界面美化之自定义qss样式表的详细步骤,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-03-03
  • Visual Studio Code (vscode) 配置C、C++环境/编写运行C、C++的教程详解(主要Windows、简要Linux)

    Visual Studio Code (vscode) 配置C、C++环境/编写运行C、C++的教程详解(主要Windo

    这篇文章主要介绍了Visual Studio Code (vscode) 配置C、C++环境/编写运行C、C++(主要Windows、简要Linux),本文通过实例截图给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-03-03

最新评论