c++11 多线程间共享数据 <c++ concurrency in action>

本章主要描述多线程之间共享数据的方法、存在问题、解决方案。

第一部分:mutex在保护共享数据中的使用

1、最简单使用:

#include<mutex>
std::mutex some_mutex;
void func(){
    some_mutex.lock();
    //访问共享数据
    ....
    some_mutex.unlock();
}

2、向lock_guard推进:

但是不推荐直接使用lock、unlock,因为unlock一定要调用,如果由于你的疏忽或前面的异常将会导致问题,再次利用RAII思想,用对象管理资源就有了标准库的std::lock_guard,在构造函数中lock,析构函数中unlock。

std::mutex some_mutex;
void func(){
    lock_guard<std::mutex> some_guard(some_mutex);
    //访问共享数据
    ....
}

3、向封装前进:

  每次数据访问都要记得加解锁,如果能让用户从加解锁中解脱就好了。将共享数据、mutex、对共享数据的访问函数(接口)封装到一起,这样用户就可以在多线程下安全使用该共享数据了。示例如下:

class EncapShareData{
    std::mutex m_mutex;
    Data m_data;
public:
    void Func(){
        std::lock_guard<std::mutex> mutexGuard;
        //对数据访问
        .....
    }
    //下述方法曝光了m_data,是危险的
    //返回共享数据的引用
    Data& DangerFunc1();
    //返回共享数据的指针
    Data* DangerFunc2();
    //参数中返回了共享数据的引用
    void DangerFunc3(Data&);
    //参数中返回了共享数据的指针
    void DangerFunc4(Data*);
    //函数f做了什么?将m_data的指针或引用保存到其他地方了(糟糕)?
    template<typename Function>
    void DangerFunc5(Function f){
        std::lock_guard<std::mutex> mutexGuard(m_mutex);
        f(m_data);
    }
    //friend!class、func...
    friend void DangerFunc6();
}

注意:示例中的DangerFunc*任意一种都会让对共享数据的多线程安全访问毁于一旦。

4、到此为止?

当你走完上述封装之路,并且确保封装类的每个接口都是多线程安全的,是不是真的就多线程安全了呢?看下面例子所示:

template<typename T>
class ThreadSafeStack{
private:
    std::stack<T> m_data;
    std::mutex m_mutex;
public:
    ThreadSafeStack(){}
    ThreadSafeStack(const ThreadSafeStack& other){
        std::lock_guard<std::mutex> lock(other.m_mutex);
        data = other.data;
    }
    ThreadSafeStack& operator=(const ThreadSafeStack&) = delete;
    //多线程安全的push
    void push(T v){
        std::lock_guard<std::mutex> lock(m_mutex);
        data.push(v);
    }
    //多线程安全的pop
    void pop(){
        std::lock_guard<std::mutex> lock(m_mutex);
        data.pop();
    }
    //stack::top返回内部元素引用,这里为了安全返回拷贝
    T top(){ return data.top(); }
    //empty/size是只读,是多线程安全的,只需要转发
    bool empty(){ return data.empty(); }
    size_t size(){ return data.size(); }
};

  ThreadSafeStack类的每一个接口单独拿出来都是线程安全的,我们知道stl中stack的入栈只需要一个函数:stack::push()就可以了,但是如果要出栈就需要一连串函数调用,先要判断栈是否为空(top并不进行是否为空检查,所以你要检查)stack::empty(),然后取得栈顶元素stack::top(),最后才能stack::pop(),如下代码所示:

if(!mStack.empty()){
    //①
    T const v=mStack.top();
    //②
    mStack.pop();
}

  单线程下这是安全的胡庸置疑。多线程线考虑以下情况:

  a、栈中只有一个元素,两个独立要pop的线程可能都走到①处,然后在②处出现在空栈上pop,悲剧!

  b、栈中有两个元素,两个独立要pop的线程可能都走到①处,但是取得的是同一个元素,然后pop两次,导致数据丢失!

  函数中每个元素访问是多线程安全的,不代表这个函数是多线程安全的,一个类中每个函数是多线程安全的,不代表将这些函的数组合是多线程安全的,如果这些组合是多线程安全的,并不代表更高层的组合是多线程安全的...。你要根据你的需要权衡提供哪一个层次的多线程安全(即确定mutex的作用范围)。比如上述的问题:类中每个单独函数都是多线程安全的,组合到一起不再多线程安全,要求的多线程范围是“函数组合”不再是单独的函数了,解决方法是将这个“函数组合”封装起来,将mutex作用范围扩展到这个组合,最简单的封装就是封装成一个单独的函数,将上面所有步骤封装成一个新的pop如下:

bool pop(T& v){
    std::lock_guard<std::mutex> lock(m_mutex);
    if(m_data.empty()) return false;
    value=data.top();
    data.pop();
    return true;
}

  mutex的作用范围要设置合理,如果太小,只能保证小范围的多线程安全,但是小范围能确保足够少的函数需要mutex同步,效率高;如果作用范围太大,可以保证大范围的线程安全,但是大范围内很多操作都是没必要同步的本身是线程安全的,这样会降低性能。

第二部分:多个mutex导致的死锁:

1、最基本的最核心的解决方法(按顺序加锁):

  写字需要纸和笔,只有一张纸、一支笔(资源有限),两个人同时决定去写字,A拿到了笔,B拿到了纸(推进顺序不当),如果他们谁都不妥协就会导致死锁。教科书中死锁产生原因:资源有限、推进顺序不当。如果规定先拿到笔才能去拿纸,在这种情况下就不会产生死锁。

  由此可知在多个mutex情况下规定加锁顺序可以避免死锁,PV操作如下:

P(pen);
P(paper);
    写字;
V(paper);
V(pen);

2、当按顺序加锁行不通时:

  按规定顺序加锁可以避免死锁,但是有的情况下这种顺序并“无法”确定,例如Swap(C,D)需要访问需要同时访问C、D的内部数据,理所当然对C、D都加锁再访问,按常规思路首先对第一个参数加锁,然后对第二个参数加锁,如下Swap(C,D)所示,那另一个线程同时执行Swap(D,C)将参数调用来了,结果如何呢?死锁!

Swap(C,D):P(C);P(D);执行内部数据交换;V(D);V(C);
Swap(D,C):P(D);P(C);执行内部数据交换;V(C);V(D);

  该如何解决呢?stl提供std::lock(mutex1,mutex2)将mutex1和mutex2的两个加锁操作当成原子一步操作(但你要记得对mutex1和mutex2进行unlock),这就不会导致死锁,std::lock要么将mutex1和mutex2都加锁也么一个都不锁。如下代码所示:

void Swap(T&C, T&D){
    if (&T == &D) return;    //对std::mutex两次lock是未定义的,从效率、安全考虑这都很必要
    std::lock(C.m_mutex, D.m_mutex);
    std::lock_guard<std::mutex> lock1(C.m_mutex, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(D.m_mutex, std::adopt_lock);
    //数据交换的操作
    .....
}

  std::adopt_lock告诉lock_guard在构造函数中不用在调用std::mutex::lock了,在前面已经调用过了。

3、hierarchical_mutex实现在运行时检查死锁的出现。

  hierarchical_mutex规则思想是:将mutex分层,规定加锁顺序是由高层到底层才能进行,底层到高层报出运行时错误,这样就可以利用编程的方法检测死锁。书中实现了hierarchical_mutex类作为可分层的mutex,先列出使用方法如下:

hierarchical_mutex high_level_mutex(10000);
hierarchical_mutex low_level_mutex(500);
void ThreadA(){
    std::lock_guard<hierarchical_mutex> lock1(high_level_mutex);
    ...    //做一些使用high_level_mutex就可以干的事
        std::lock_guard<hierarchical_mutex> lock1(low_level_mutex);
    ...    //需要两个mutex同时加锁才可以干的事
}
void ThreadB(){
    std::lock_guard<hierarchical_mutex> lock1(low_level_mutex);
    ...    //做一些使用low_level_mutex就可以干的事
        //对高低层mutex加锁的情况下,对高层mutex加锁,不符合规定的顺序,抛出异常!
        std::lock_guard<hierarchical_mutex> lock1(high_level_mutex);
}

  ThreadA符合hierarchical_mutex使用规定不会出现死锁是多线程安全的,ThreadB没按hierarchical_mutex 规定,有出现死锁的危险,在运行时就抛出异常。

  hierarchical_mutex实现方法,要能使用lock_guard对hierarchical_mutex进行管理必须实现lock/unlock/try_lock方法;为了实现层次间的比较进而决定能不能加锁,需要记录准备加锁mutex的层号、该线程中当前已经加锁的mutex的层号、在解锁时恢复原先现场就要记录先前mutex的层号,综上hierarchical_mutex定义如下:

class hierarchical_mutex{
    std::mutex internal_mutex;
    //准备加锁mutex的层号
    const unsigned hierarchical_value;
    //在解锁时恢复原先现场就要记录先前mutex的层号
    unsigned previous_hierarchical_value;
    //该线程中当前已经加锁的mutex的层号
    static thread_local unsigned this_thread_hierarchical_value;
    //检查是否满足hierarchical_mutex规则
    void check_for_hierarchical_violation(){
        if (this_thread_hierarchical_value <= hierarchical_value){
            throw std::logic_error(“mutex hierarchical violated!”);
        }
    }
    void update_hierarchical_value(){
        previous_hierarchical_value = this_thread_hierarchical_value;
        this_thread_hierarchical_value = hierarchical_value;
    }
public:
    explicit hierarchical_mutex(unsigned value) :hierarchical_value(value), previous_hierarchical_value(0){}
    //满足hierarchical_mutex规则时加锁并更新内部记录变量
    void lock(){
        check_for_hierarchical_violation();
        internal_mutex.lock();
        update_hierarchical_value;
    }
    //恢复到调用lock之前的现场,解锁
    void unlock(){
        this_thread_hierarchical_value = previous_hierarchical_value;
        internal_mutex.unlock();
    }
    bool try_lock(){
        check_for_hierarchical_violation();
        if (!internal_mutex.try_lock())
            return false;
        update_hierarchical_value();
        return true;
    }
};

//使用UNSIGND_MAX初始化表明刚开始任何层的mutex都可以加锁成功
thread_local unsigned hierarchical_mutex::this_thread_hierarchical_value(UNSIGND_MAX);

  标注:mutex::try_lock()检查mutex是否可以成功lock,如果可以就lock,如果不行立刻返回false.

第三部分:扩展

一、unique_lock:

1、相对lock_guard加锁、解锁更灵活的控制。

  内部保存的是相关联mutex的状态,使用std::lock调用或std::unique_lock::lock()或使用std::unique_lock(mutex)构造时打开状态,使用unique_lock::unlock时关闭状态,在析构时候检查状态以决定是否调用std::mutex::unlock(),灵活就在随时调用lock、unlock。

std::unique_lock lock(m_mutex,std::defer_lock);    //std::defer_lock说明定义时对m_mutex不加锁
....
std::lock(lock);        //需要加锁时再加锁
....
lock.unlock();        //使用结束手动解锁

2、对mutex的控制权转移:

//*1作为参数
void f2(std::unique_lock&& p){ ..此范围内p关联的m_mutex已经加锁.. }
void f1(){
    std::unique_lock lock(m_mutex);        //对m_mutex加锁
    f2(lock);        //m_mutex管理权限转移到f2中
}
//*2作为返回值
unique_lock f2(){
    std::unique_lock lock(m_mutex);
    prepareData();
    return lock;
}
void f1(){
    std::unique_lock lock(f2());
    processData();
}

  unique_lock内部保存的是关联mutex的状态标记,可以利用unique_lock::owe_lock()检查关联的mutex是否已经加锁。管理权转移为什么不用lock_guard呢?从源代码分析可知lock_guard没有拷贝构造、赋值、移动构造、移动赋值,意味着不能作为参数、返回值传递,但是unique_lock跟unique_ptr一样虽然没有拷贝构造、赋值,但有移动构造、移动赋值,“天生”是用做权限转移的。

二、Lazy-initialization:

单线程中:

std::shared_ptr<some_resource> resource_ptr;
void foo(){
    if (!resource_ptr){
        resource_ptr.reset(new some_resource);
    }
    resource_ptr->do_something();
}

多线程中:

std::shared_ptr<some_resource> resource_ptr;
std::mutex resource_mutex;
void foo(){
    std::unique_lock<std::mutex> lock(resource_mutex);
    if (!resource_ptr){
        resource_ptr.reset(new some_resource);
    }
    lock.unlock();
    resource_ptr->do_something();
}

  我们要的是只在resource_ptr还没初始化时才加锁、解锁,这个版本任何时候进入foo都加锁、解锁,明显效率下降。然后就有了“infamous”双重锁:

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

  看似很合理,但这却变成了一个让很多学者头疼的问题。问题出在②处对resource_ptr指针的赋值和some_resource构造函数的调用谁先谁后不确定(编译器要重排代码顺序,究竟怎么重排没规定),如果首先对resource_ptr指针赋值但是some_resource构造函数还没有调用,另一个线程在①处检查到resource_ptr有值了(true),立刻执行③,其实是错误的。

  标准库提供了一种解决方案,具体怎么解决的很复杂参考http://blog.jobbole.com/52164/。这里只谈使用方法,你只需知道这是多线程安全的并且比在正确情况下的双重检查锁效率还高,代码示例如下:

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

  你可以将此方法很容易扩展到多线程安全的单例模式,实现真正的多线程安全。

三、保护很少更新,但是经常是只读的数据:

  可以想到使用读者优先,你可以自己利用mutex去设计自己的读者优先的函数,标准库并没有提供任何读写锁。但是boost库提供了boost::shared_mutex和boost::shared_lock函数可以很简单的解决这个问题:当使用lock_guard或unique_lock对boost::shared_mutex加锁后,对该boost::shared_mutex的获取方式(lock_guard/unique_lock/shared_lock)都会阻塞;当使用boost::shared_lock对该boost::shared_mutex加锁后,使用lock_guard和unique_lock对该boost::shared_lock的获取就会阻塞,但是使用shared_lock对该boost::shared_mutex的获取就不会阻塞。使用实例如下:

#include <map>
#include <string>
#include <mutex>
#include <boost/thread/shared_mutex.hpp>
class dns_entry;
class dns_cache
{
    std::map<std::string, dns_entry> entries;
    mutable boost::shared_mutex entry_mutex;
public:
    //读者
    dns_entry find_entry(std::string const& domain) const
    {
        boost::shared_lock<boost::shared_mutex> lk(entry_mutex);
        //阻塞写者但不阻塞读者
        std::map<std::string, dns_entry>::const_iterator const it =
            entries.find(domain);
        return (it == entries.end()) ? dns_entry() : it->second;
    }
    //写者
    void update_or_add_entry(std::string const& domain, dns_entry const& dns_details)
    {
        std::lock_guard<boost::shared_mutex> lk(entry_mutex);
        //将其他的写者和所有的读者都阻塞
        entries[domain] = dns_details;
    }
};

  对recursive_mutex的使用,作者不推荐“Most of the time, if you think you want a recursive mutex, you probably need to change your design instead”,这里就不再详述了。

  本帖全自己在阅读《c++ concurrency in action》中的总结,如果对你有帮助,请点个赞^-^

时间: 2024-10-20 18:45:12

c++11 多线程间共享数据 <c++ concurrency in action>的相关文章

详解 Qt 线程间共享数据(用信号槽方式)

使用共享内存.即使用一个两个线程都能够共享的变量(如全局变量),这样两个线程都能够访问和修改该变量,从而达到共享数据的目的. Qt 线程间共享数据是本文介绍的内容,多的不说,先来啃内容.Qt线程间共享数据主要有两种方式: 使用共享内存.即使用一个两个线程都能够共享的变量(如全局变量),这样两个线程都能够访问和修改该变量,从而达到共享数据的目的: 使用singal/slot机制,把数据从一个线程传递到另外一个线程. 第一种办法在各个编程语言都使用普遍,而第二种方式倒是QT的特有方式,下面主要学习一

详解 Qt 线程间共享数据(使用signal/slot传递数据,线程间传递信号会立刻返回,但也可通过connect改变)

使用共享内存.即使用一个两个线程都能够共享的变量(如全局变量),这样两个线程都能够访问和修改该变量,从而达到共享数据的目的. Qt 线程间共享数据是本文介绍的内容,多的不说,先来啃内容.Qt线程间共享数据主要有两种方式: 使用共享内存.即使用一个两个线程都能够共享的变量(如全局变量),这样两个线程都能够访问和修改该变量,从而达到共享数据的目的: 使用singal/slot机制,把数据从一个线程传递到另外一个线程. 第一种办法在各个编程语言都使用普遍,而第二种方式倒是QT的特有方式,下面主要学习一

线程系列03,多线程共享数据,多线程不共享数据

多线程编程,有时希望每个线程的数据相互隔离互不影响,有时却希望线程间能共享数据,并保持同步.本篇体验多线程共享和不共享数据. □ 多线程不共享数据 对于多线程,CLR到底是怎样为它们分配内存栈空间呢?是"一个萝卜一个坑",每个线程都有自己的栈空间:还是"大树底下好乘凉",所有的线程共享同一个栈空间? 我们让2个线程执行相同的静态方法,用到相同的变量,通过打印变量来求证多线程栈空间的分配情况. class Program { static void Main(stri

线程间共享数据的一个例子

[申明:本文仅限于自我归纳总结和相互交流,有纰漏还望各位指出. 联系邮箱:[email protected]] 题目:输入一个整形数组,数组里有正数也有负数. 数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和. 求所有子数组的和的最大值.要求时间复杂度为O(n). 题目分析: 一.如果数组中全部为负数,则返回最大负数值即可 二.当既有正数也有负数的时候: (1)从左往右叠加,如果当前叠加值小于或者等于0,则放弃,叠加总和清0(加一个负数或者0是毫无意义的),从此位置继续重新叠加 (

利用内存映射文件在两个进程间共享数据 转

private hMapFile: THandle; MapFilePointer: Pointer; public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); begin hMapFile := CreateFileMapping ( $FFFFFFFF, // 特殊内存映射句柄 nil, page_

windows核心编程之进程间共享数据

有时候我们会遇到window进程间共享数据的需求,例如说我想知道系统当前有多少某个进程的实例. 我们能够在程序中定义一个全局变量.初始化为0.每当程序启动后就加1.当然我们我们能够借助第三方介质来储存这个变量,然后解析. 这样做必须做到先写入后解析.不能实时更新数据.假设不考虑其它储存介质.仅仅是进程中的通信,应该怎么做呢?windows提供了一些可行的方法,以下介绍经常使用的两种. 一.共享数据段 #include "stdafx.h" #include <Windows.h&

python 进程间共享数据 (二)

Python中进程间共享数据,除了基本的queue,pipe和value+array外,还提供了更高层次的封装.使用multiprocessing.Manager可以简单地使用这些高级接口. Manager()返回的manager对象控制了一个server进程,此进程包含的python对象可以被其他的进程通过proxies来访问.从而达到多进程间数据通信且安全. Manager支持的类型有list,dict,Namespace,Lock,RLock,Semaphore,BoundedSemaph

【转】VC 利用DLL共享区间在进程间共享数据及进程间广播消息

1.http://blog.csdn.net/morewindows/article/details/6702342 在进程间共享数据有很多种方法,剪贴板,映射文件等都可以实现,这里介绍用DLL的共享区间在进程间共享数据,及共享数据有变化时及时的反馈给各相关进程. 一.在DLL中设置共享区间 在DLL中是用数据段来实现共享区间的,有了这个共享区间,各进程可以方便的共享数据. 1.先用#pragma data_seg(Name)设置名为Name的数据段. 2.再用#pragma comment(l

C++11并发编程4------线程间共享数据

举个例子: 刚参加工作的你,只能租房住,嫌房租贵就和别人合租了,两个人住一起只有一个洗手间,每天早上起床的时候,如果你室友在洗手间,你就只能等着,如果你强行进去,那画面就不可描述了.同样的问题,如果多个线程共享一个数据,并且对数据有读有写,那就需要注意共享数据的保护了. 使用互斥量保护共享数据: 当访问共享数据前,使用互斥量奖相关数据锁住,当访问结束后,再将数据解锁.互斥量是C++中一种最通用的数据保护机制. C++中使用互斥量: 原文地址:https://www.cnblogs.com/418