关于线程安全的困惑

时间:2018-01-25 14:10:19

标签: c++ c++11 thread-safety

我是并发世界的新手,但从我读过的内容中我理解下面的程序在执行时未定义。如果我理解正确,这不是线程安全的,因为我同时以非原子方式读取/写入shared_ptr和counter变量。

#include <string>
#include <memory>
#include <thread>
#include <chrono>
#include <iostream>


struct Inner {
    Inner() {
        t_ = std::thread([this]() {
            counter_ = 0;
            running_ = true;
            while (running_) {
                counter_++;
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
            }
        });
    }

    ~Inner() {
        running_ = false;
        if (t_.joinable()) {
            t_.join();
        }
    }


    std::uint64_t counter_;
    std::thread t_;
    bool running_;
};


struct Middle {

    Middle() {
        data_.reset(new Inner);
        t_ = std::thread([this]() {
            running_ = true;
            while (running_) {
                data_.reset(new Inner());
                std::this_thread::sleep_for(std::chrono::milliseconds(1000));
            }
        });
    }

    ~Middle() {
        running_ = false;
        if (t_.joinable()) {
            t_.join();
        }
    }

    std::uint64_t inner_data() {
        return data_->counter_;
    }

    std::shared_ptr<Inner> data_;
    std::thread t_;
    bool running_;
};

struct Outer {

    std::uint64_t data() {
        return middle_.inner_data();
    }

    Middle middle_;
};




int main() {

    Outer o;
    while (true) {
        std::cout << "Data: " << o.data() << std::endl;
    }

    return 0;
}

我的困惑来自于:

  1. data_->counterMiddle::inner_data的访问权限是否安全?
  2. 如果线程A有一个成员shared_ptr<T> sp并且决定更新它而线程B确实shared_ptr<T> sp = A::sp那么复制和销毁是线程安全的吗?或者我冒着副本失败的风险,因为对象正在被销毁。
  3. 在什么情况下(我可以用某种工具查看吗?)未定义可能意味着std::terminate?我怀疑在我的一些生产代码中发生了类似上面的事情,但我无法确定,因为我对1和2感到困惑,但这个小程序已经运行了几天,因为我写了它并没有任何反应。

    可以在https://godbolt.org/g/saHz94

    处查看代码

2 个答案:

答案 0 :(得分:3)

  

在Middle :: inner_data中访问数据_-&gt;计数器是否安全?

没有;这是一场竞争。根据标准,只要您允许从多个线程对同一变量进行非同步访问,并且至少有一个线程可能修改该变量,它就是未定义的行为。

实际上,您可能会看到以下几种不需要的行为:

  1. 读取counter_的值的线程读取计数器的“旧”值(很少或从不更新),因为不同的处理器核心相互独立地缓存变量(使用atomic_t会避免这个问题,因为那时候编译器会意识到你打算以不同步的方式访问这个变量,它会知道采取预防措施来防止这个问题)

  2. 当线程A被启动时,线程A可能会读取data_ shared_pointer指向的地址,并且即将取消引用该地址并从它指向的Inner结构读取CPU由线程B执行。线程B执行,在线程B执行期间,旧的Inner结构被删除,data_ shared_pointer设置为指向新的Inner结构。然后线程A再次返回到CPU,但由于线程A已经在内存中具有旧指针值,因此它取消引用旧值而不是新值,最终从释放/无效内存中读取。同样,这是未定义的行为,因此原则上任何事情都可能发生;在实践中,你可能会看到没有明显的不当行为,或偶尔会出现错误/垃圾价值,或者可能是崩溃,这取决于它。

  3.   

    如果线程A有成员shared_ptr sp并决定更新它   而线程B确实shared_ptr sp = A :: sp将复制和   破坏是线程安全的?或者我冒着副本失败的风险,因为   物体正在被摧毁。

    如果您只是重新定位shared_ptrs本身(即将它们更改为指向不同的对象)而不修改它们指向的T对象,则应该是线程安全的AFAIK。但是,如果你正在修改T对象本身的状态(即你的例子中的Inner对象),这不是线程安全的,因为你可以有一个线程从对象读取而另一个线程正在写入它(删除对象可以看作是写入它的特殊情况,因为它肯定会改变对象的状态)

      

    在什么情况下(我可以用一些工具查看吗?)是   undefined可能意味着std :: terminate?

    当您点击未定义的行为时,它非常依赖于程序,编译器,操作系统和硬件架构的细节。原则上,未定义的行为意味着任何事情(包括按照您的意图运行的程序!)都可能发生,但您不能依赖任何特定的行为 - 这就是使未定义的行为如此邪恶的原因。

    特别是,具有竞争条件的多线程程序通常可以运行数小时/数天/周,然后有一天时机恰到好处并且崩溃或计算出错误的结果。由于这个原因,种族条件很难重现。

    至于可能调用terminate()的时候,如果故障导致运行时环境检测到错误状态,则会调用terminate()(即它会破坏运行时环境进行完整性检查的数据结构,例如,在一些实现中,堆的元数据)。是否实际发生取决于堆的实现方式(从一个操作系统和编译器到另一个操作系统不同)以及故障引入的损坏类型。

答案 1 :(得分:1)

线程安全是线程之间的操作,通常不是绝对的。

当另一个线程写入变量而另一个线程的写入与您的读取或写入之间没有同步时,您无法读取或写入变量。这样做是未定义的行为。

未定义可能意味着什么。程序崩溃。程序读取不可能的值。程序格式化硬盘。程序会将您的浏览器历史记录通过电子邮件发送给所有联系人。

非同步整数访问的一个常见情况是编译器将多个读取优化为一个值并且不重新加载它,因为它可以证明没有定义的方式可以有人修改该值。或者,CPU内存缓存执行相同的操作,因为您没有进行同步。

对于指针,可能会出现类似或更糟的问题,包括跟随悬空指针,破坏内存,崩溃等。

There are now atomic operations you can perform on shared pointers.,以及atomic<shared_ptr<?>>