共享数据和线程

共享数据带来的问题

条件竞争

条件竞争产生

并发中竞争条件的形成,取决于一个以上线程的相对执行顺序,每个线程都抢着完成自己的任务。大多数情况下,即使改变执行顺序,也是良性竞争,结果是可以接受的。例如有两个线程同时向一个处理队列中添加任务,由于系统提供的不变量保持不变,所以先后顺序并不会造成影响。不变量遭到破坏以后,才会产生条件竞争,这种竞争方式通常表示为恶性条件竞争。
恶性条件竞争通常发生于对多于一个的数据块的修改的时候,操作访问两个独立的数据块,独立的指令将会对数据块进行修改,并且可能一个线程正在进行时,另一个线程就对数据块进行了访问。因为出现的概率太小,条件竞争很难查找,并且也很难进行浮现。当系统负载增加的时候,随着执行数量的增加,执行序列的问题复现的概率也在增加,但是这只会出现在负载比较大的时候。条件竞争通常是时间敏感的,所以程序以调试模式进行运行的时候,他们将会完全消失。

避免恶性条件竞争

  • 对数据结构采用某种保护机制,确保只有进行修改的线程才能看到不变量被破坏的中间状态。从其他的线程来看,修改不是已经完成的,就是还没有开始。
  • 对数据结构和不变量的设计进行修改,修改完成的结构必须可以完成一系列不可分割的变化,也就是保证每个不变量保持稳定的状态,即所谓的无锁编程。
  • 使用事物的方式进行数据结构的更新。当所需的一些数据和读取都存储在事务日志中,然后将之前的操作合并为一步再进行提交。当数据结构被另一个线程修改之后,或者处理已经重启的情况下,提交就会无法进行。这被称作“软件事物内存”

使用互斥量保护数据

使用互斥量保护共享数据

当访问共享数据的前,使用互斥量将相关的数据锁住,当访问结束之后,再将数据进行解锁。当一个线程使用特定互斥量锁住共享数据时候,其他的线程想要访问锁住的数据,都必须等到之前的那个线程对数据进行解锁后,才能进行访问。这就保证了所有线程都可以看到共享数据,而不破坏不变量。

C++中使用互斥量

C++中通过实例化std::mutex创建互斥量,通过调用成员函数lock()进行上锁,unlock()进行解锁。C++标准库为互斥量提供了一个RAII语法的模板类std::lack_guard,其会在构造的时候提供已经上锁的互斥量,并且在析构的时候进行解锁,从而保证了一个已经上锁的互斥量都会被正确的解锁。

#include<mutex>
#include<list>
#include<algorithm>
using namespace std;
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);
}

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

在某些情况下,使用全局变量没有问题,但是在大多数的情况下,互斥量通常会和保护的数据放在同一个类中,而不是定义成全局变量。
将其放在同一个类中,就可以将其联系在一起,也可以对类的功能进行封装,并且进行数据的保护。在这种情况下,函数add_to_list和list_contains都可以作为这个类的成员函数。 互斥量和要保护的数据,在类中都要定义为private成员。当所有成员函数都会在调用的时候对数据进行上锁,结束的时候对数据进行解锁,就可以保证数据访问的时候,不变量不会被破坏。

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;

class some_data{
    int a;
    string b;
public:
some_data():a(10),b("This is a test"){};
void do_something(){
    cout<<b<<"\n";
    cout<<a<<"\n";
};
void reset_a(int x){
    a=x;
}
void reset_b(string s){
    b=s;
}

};

//构造一个data_wrapper 类对data数据结构加以保护
//同时可以使用参数传递的方式 传入处理函数
//这里只使用了一个互斥量 保护的是data整体
class data_wrapper{
private:
    some_data data;
    std::mutex m;
public:
    template<typename Function>
    void process_data(Function func){
    std::lock_guard<std::mutex> l(m);
    func(data);
}
};

void malicious_function(some_data& protected_data){
    protected_data.do_something();
    protected_data.reset_a(10);
    protected_data.reset_b("another test");
    protected_data.do_something();
}


int main(){
data_wrapper x;

x.process_data(malicious_function);
return 0;
}

当其中的一个成员函数返回的是保护数据的指针或者引用的时候,会破坏对数据的保护。

精心组织代码来保护共享数据

使用互斥量保护数据,并不是仅仅在每一个成员函数中都加入一个std::lock_guard对象那么简单。一个迷失的指针或者引用,会将这种保护形同虚设。只要没有成员函数通过返回值或者是输出参数的形式向其调用者返回指向受保护数据的指针或者引用,数据就是安全的。
在确保成员函数不会传出指针或者引用的同时,检查成员函数是否通过指针或者引用的方式来调用也是重要的。函数可能没在互斥量保护的区域内,存储着指针或者引用,这样就是危险的。更为危险的是,将保护数据作为一个运行时参数。

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;

class some_data{
    int a;
    string b;
public:
some_data():a(10),b("This is a test"){};
void do_something(){
    cout<<b<<"\n";
    cout<<a<<"\n";
};
void reset_a(int x){
    a=x;
}
void reset_b(string s){
    b=s;
}

};

//构造一个data_wrapper 类对data数据结构加以保护
//同时可以使用参数传递的方式 传入处理函数
//这里只使用了一个互斥量 保护的是data整体
class data_wrapper{
private:
    some_data data;
    std::mutex m;
public:
    template<typename Function>
    void process_data(Function func){
    std::lock_guard<std::mutex> l(m);
    func(data);
}
};
//这里使用了一个不受保护的指针 导致 在没有互斥量的情况下 也可以进行对函数的访问和改变
//是错误的需要进行避免的操作
some_data * unprotected_data;
void malicious_function(some_data& protected_data){
    protected_data.do_something();
    protected_data.reset_a(10);
    protected_data.reset_b("another test");
    protected_data.do_something();
    unprotected_data=&protected_data;
}


int main(){
data_wrapper x;

x.process_data(malicious_function);
unprotected_data.do_something();
return 0;
}
使用接口内在的竞争条件

在使用了互斥量或者是其他机制保护共享数据的时候,也需要避免接口内部的条件竞争。
以双向链表为例,为了可以线程安全的删除一个节点,需要确保防止对三个节点(待删除节点以及前后相邻的节点)的并发访问。如果只是对指向每个节点的指针进行了访问保护,就和没有使用互斥量是一样的,条件竞争仍然是存在的。除了指针,整个数据结构和整个删除操作都是需要进行保护的。这种情况下最简单的解决方案就是使用互斥量保护整个链表。
构建一个类似于std::stack结构的栈,除了构造函数和swap()操作之外,还需要提供push(),pop(),top()等操作。

数据保护的替代方案

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容