欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

c++11之线程共享数据

程序员文章站 2022-06-23 09:29:49
共享数据通常我们启用多线程都是同时处理关键资源,会经常碰到资源竞争的情况,如果多个线程同时访问同个资源,会造成未知的错误。当线程在访问共享数据的时候,必须定一些规矩,用来限定线程可访问的数据位。还有,一个线程更新了共享数据,需要对其他线程进行通知。从易用性的角度,同一进程中的多个线程进行数据共享,有利有弊。错误的共享数据使用是产生并发bug的一个主要原因。本章就以在C++中进行安全的数据共享为主题。避免上述及其他潜在问题的发生的同时,将共享数据的优势发挥到最大。共享数据带来的问题当涉及到共享数据时...

共享数据

通常我们启用多线程都是同时处理关键资源,会经常碰到资源竞争的情况,如果多个线程同时访问同个资源,会造成未知的错误。
当线程在访问共享数据的时候,必须定一些规矩,用来限定线程可访问的数据位。还有,一个线程更新了共享数据,需要对其他线程进行通知。从易用性的角度,同一进程中的多个线程进行数据共享,有利有弊。错误的共享数据使用是产生并发bug的一个主要原因。
本章就以在C++中进行安全的数据共享为主题。避免上述及其他潜在问题的发生的同时,将共享数据的优势发挥到最大。

共享数据带来的问题

当涉及到共享数据时,问题很可能是因为共享数据修改所导致。如果共享数据是只读的,那么只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多麻烦。这种情况下,就必须小心谨慎,才能确保一切所有线程都工作正常。
不变量(invariants)的概念对程序员们编写的程序会有一定的帮助——对于特殊结构体的描述;比如,“变量包含列表中的项数”。不变量通常会在一次更新中被破坏,特别是比较复杂的数据结构,或者一次更新就要改动很大的数据结构。
c++11之线程共享数据

图中b和c在相同的方向上指向和原来已经不一致了,这就破坏了不变量。
线程间潜在问题就是修改共享数据,致使不变量遭到破坏。当不做些事来确保在这个过程中不会有其他线程进行访问的话,可能就有线程访问到刚刚删除一边的节点;这样的话,线程就读取到要删除节点的数据(因为只有一边的连接被修改,如图3.1(b)),所以不变量就被破坏。破坏不变量的后果是多样,当其他线程按从左往右的顺序来访问列表时,它将跳过被删除的节点。在一方面,如有第二个线程尝试删除图中右边的节点,那么可能会让数据结构产生永久性的损坏,使程序崩溃。无论结果如何,都是并行代码常见错误:条件竞争(race condition)。

条件竞争

几个窗口同时卖票,在票快卖完的时候就是一个典型条件竞争的例子。
并发中竞争条件的形成,取决于一个以上线程的相对执行顺序,每个线程都抢着完成自己的任务。大多数情况下,即使改变执行顺序,也是良性竞争,其结果可以接受。例如,有两个线程同时向一个处理队列中添加任务,因为系统提供的不变量保持不变,所以谁先谁后都不会有什么影响。当不变量遭到破坏时,才会产生条件竞争,比如双向链表的例子。并发中对数据的条件竞争通常表示为“恶性”(problematic)条件竞争,我们对不产生问题的良性条件竞争不感兴趣。C++标准中也定义了数据竞争(data race)这个术语,一种特殊的条件竞争:并发的去修改一个独立对象,数据竞争是(可怕的)未定义行为(undefine behavior)的起因。
恶性条件竞争通常发生于完成对多于一个的数据块的修改时,例如,对两个连接指针的修改(如图3.1)。因为操作要访问两个独立的数据块,独立的指令将会对数据块将进行修改,并且其中一个线程可能正在进行时,另一个线程就对数据块进行了访问。因为出现的概率太低,条件竞争很难查找,也很难复现。如CPU指令连续修改完成后,即使数据结构可以让其他并发线程访问,问题再次复现的几率也相当低。当系统负载增加时,随着执行数量的增加,执行序列的问题复现的概率也在增加,这样的问题只可能会出现在负载比较大的情况下。条件竞争通常是时间敏感的,所以程序以调试模式运行时,它们常会完全消失,因为调试模式会
影响程序的执行时间(即使影响不多)。
当你以写多线程程序为生,条件竞争就会成为你的梦魇;编写软件时,我们会使用大量复杂的操作,用来避免恶性条件竞争。

避免恶性条件竞争

这里提供一些方法来解决恶性条件竞争,最简单的办法就是对数据结构采用某种保护机制,确保只有进行修改的线程才能看到不变量被破坏时的中间状态。从其他访问线程的角度来看,修改不是已经完成了,就是还没开始。C++标准库提供很多类似的机制,下面会逐一介绍。
另一个选择是对数据结构和不变量的设计进行修改,修改完的结构必须能完成一系列不可分割的变化,也就是保证每个不变量保持稳定的状态,这就是所谓的无锁编程(lock-free programming)。不过,这种方式很难得到正确的结果。如果到这个级别,无论是内存模型上的细微差异,还是线程访问数据的能力,都会让工作变的复杂。
保护共享数据结构的最基本的方式,是使用C++标准库提供的互斥量(mutex)。

C++中使用互斥量

互斥量是C++中一种最通用的数据保护机制,但它不是“银弹”;精心组织代码来保护正确的数据,并在接口内部避免竞争条件是非常重要的。而且互斥量自身也有问题,也会造成死锁,或是对数据保护的太多(或太少)。所以在何时何地使用互斥量是我们最需要关注的问题。
C++中通过实例化 std::mutex 创建互斥量,通过调用成员函数lock()进行上锁,unlock()进行解锁。不过,不推荐实践中直接去调用成员函数,因为调用成员函数就意味着,必须记住在每个函数出口都要去调unlock(),也包括异常的情况。C++标准库为互斥量提供了一个RAII语法的模板类 std::lock_guard ,其会在构造的时候提供已锁的互斥量,并在析构的时候进行解锁,从而保证了一个已锁的互斥量总是会被正确的解锁。

#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list; // 1
std::mutex some_mutex; // 2
void add_to_list(int new_value)
{
    std::lock_guard<std::mutex> guard(some_mutex); // 3
    some_list.push_back(new_value);
} 

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

以上代码示范了如何对关键资源上锁解决条件竞争问题。
需要主要的一点是当其中一个函数返回的是保护数据的指针或引用时,会破坏对数据的保护。因为具有访问能力的指针或引用可以访问(并可能修改)被保护的数据,而不会被互斥锁限制。互斥量保护的数据需要对接口的设计相当谨慎,要确保互斥量能锁住任何对保护数据的访问,并且不留后门。

精心组织代码来保护共享数据
不要让任何代码脱离保护。在正常情况下,我们都能会小心地使关键资源在我们的控制之下,良好的运行,但在一些意外的时候,可能会跳出我们的控制,导致在最不该出错的时候,给我们一个意外惊喜。
以下是资源不受保护的非典型实例:

class some_data
{
    int a;
    std::string b;
public:
    void do_something();
};
class data_wrapper
{
    p
        rivate :
    some_data data;
    std::mutex m;
public:
    template<typename Function>
    void process_data(Function func)
    {
        std::lock_guard<std::mutex> l(m);
        func(data); // 1 传递“保护”数据给用户函数
    }
};
some_data* unprotected;
void malicious_function(some_data& protected_data)
{
    unprotected = &protected_data;
} d
ata_wrapper x;
void foo()
{
    x.process_data(malicious_function); // 2 传递一个恶意函数
    unprotected->do_something(); // 3 在无保护的情况下访问保护数据
}

本文地址:https://blog.csdn.net/a123a55/article/details/107636134