c++ - 关于线程安全的困惑

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

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

#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_->counter安全 Middle::inner_data
  2. 如果线程A有一个成员shared_ptr<T> sp并决定在线程 B 执行 shared_ptr<T> sp = A::sp 时更新它复制和销毁是线程安全的吗?或者我是否冒着复制失败的风险,因为对象正在被销毁。

在什么情况下(我可以用一些工具检查这个吗?)未定义可能意味着 std::terminate ?我怀疑在我的一些生产代码中发生了类似上述的事情,但我不能确定,因为我对 1 和 2 感到困惑,但是这个小程序自从我编写它以来已经运行了好几天,但没有任何反应。

可以在此处查看代码 https://godbolt.org/g/saHz94

最佳答案

Is the access to data_->counter safe in Middle::inner_data?

没有;这是一个竞争条件。根据标准,任何时候您允许多个线程对同一变量进行非同步访问都是未定义的行为,并且至少有一个线程可能会修改该变量。

实际上,您可能会看到以下几种不受欢迎的行为:

  1. 读取 counter_ 值的线程读取了 counter 的“旧”值(很少或从不更新),因为不同的处理器内核彼此独立地缓存变量(使用 atomic_t 可以避免这个问题,因为那时编译器会知道您打算以非同步方式访问此变量,并且会知道采取预防措施来防止出现此问题)

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

If thread A has a member shared_ptr sp and decides to update it while thread B does shared_ptr sp = A::sp will the copy and destruction be threadsafe? Or do I risk the copy failing because the object is in the process of being destroyed.

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

Under what circumstances (can I check this with some tool?) is undefined likely to mean std::terminate?

当您遇到未定义的行为时,它在很大程度上取决于您的程序、编译器、操作系统和硬件架构的细节。原则上,未定义行为意味着任何事情(包括按您预期运行的程序!)都可能发生,但您不能依赖任何特定行为——这就是使未定义行为如此邪恶的原因。

特别是,对于具有竞争条件的多线程程序来说,正常运行数小时/数天/数周是很常见的,然后有一天时机正好,它崩溃或计算出错误的结果。由于这个原因,竞争条件可能真的很难重现。

至于何时可以调用 terminate(),如果故障导致运行时环境检测到的错误状态(即它破坏了运行时环境对其进行完整性检查的数据结构,例如,在某些实现中,堆的元数据)。这是否真的发生取决于堆是如何实现的(这因操作系统和编译器而异)以及错误引入的损坏类型。

关于c++ - 关于线程安全的困惑,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48444883/

相关文章:

.net - 将 ObjectContext 存储在 ASP.NET 中的线程静态变量中是否安全?

ios - NSTimer作为后台线程

C++ 在全局变量不合适时保持计数

c++ - 在子类中填充静态类成员

c++ - 如何使用模板模板参数为该方法不需要通用接口(interface)的 STL 容器实现通用方法

java - 线程安全地循环通过 ConcurrentHashMap,无阻塞

c++ - 将 32 0/1 值打包到单个 32 位变量的位中的最快方法是什么?

C++ 通过访问器返回一个字符串

C++ 从注册表中获取 Windows 产品 ID

c++ - 任意功能的定时器