22

源自此问题与此问题相关

如果我在一个线程中构造一个对象,然后将指向它的引用/指针传递给另一个线程,那么其他线程在没有显式锁定/内存屏障的情况下访问该对象是否是线程不安全的?

// thread 1
Obj obj;

anyLeagalTransferDevice.Send(&obj);
while(1); // never let obj go out of scope

// thread 2
anyLeagalTransferDevice.Get()->SomeFn();

或者:是否有任何合法的方式来在线程之间传递数据,而不强制执行与线程接触的所有其他内容有关的内存排序?从硬件的角度来看,我看不出有任何理由不应该这样做。

澄清; 问题是关于缓存一致性、内存排序等等。在线程 2 的内存视图包括构造所涉及的写入之前,线程 2 可以获取并使用指针obj吗?错过引用 Alexandrescu(?) “一个恶意的 CPU 设计者和编译器编写者会合谋建立一个符合标准的系统来打破这种局面吗?”

4

5 回答 5

17

关于线程安全的推理可能很困难,而且我不是 C++11 内存模型的专家。但是,幸运的是,您的示例非常简单。我重写了这个例子,因为构造函数是无关紧要的。

简化示例

问题:下面的代码正确吗?或者执行会导致未定义的行为

// Legal transfer of pointer to int without data race.
// The receive function blocks until send is called.
void send(int*);
int* receive();

// --- thread A ---
/* A1 */   int* pointer = receive();
/* A2 */   int answer = *pointer;

// --- thread B ---
           int answer;
/* B1 */   answer = 42;
/* B2 */   send(&answer);
           // wait forever

答:的内存位置可能存在数据竞争answer,因此执行会导致未定义的行为。详情见下文。


数据传输的实现

当然,答案取决于函数sendreceive. 我使用以下无数据竞争的实现。请注意,只使用了一个原子变量,所有内存操作都使用std::memory_order_relaxed. 基本上这意味着,这些函数不限制内存重新排序。

std::atomic<int*> transfer{nullptr};

void send(int* pointer) {
    transfer.store(pointer, std::memory_order_relaxed);
}

int* receive() {
    while (transfer.load(std::memory_order_relaxed) == nullptr) { }
    return transfer.load(std::memory_order_relaxed);
}

内存操作顺序

在多核系统上,一个线程可以看到与其他线程看到的不同顺序的内存变化。此外,编译器和 CPU 都可以在单个线程中重新排序内存操作以提高效率——而且它们一直都在这样做。原子操作std::memory_order_relaxed不参与任何同步,也不强加任何顺序。

在上面的例子中,允许编译器重新排序线程 B 的操作,并在 B1 之前执行 B2,因为重新排序对线程本身没有影响。

// --- valid execution of operations in thread B ---
           int answer;
/* B2 */   send(&answer);
/* B1 */   answer = 42;
           // wait forever

数据竞赛

C++11 对数据竞争的定义如下(N3290 C++11 草案):“如果程序的执行包含不同线程中的两个冲突操作,则程序的执行包含数据竞争,其中至少一个不是原子的,并且两者都不是发生在另一个之前。任何此类数据竞争都会导致未定义的行为。” 并且该术语发生在之前在同一文档中的较早定义。

在上面的例子中,B1 和 A2 是相互冲突的非原子操作,两者都不先发生。这很明显,因为我在上一节中已经表明,两者可以同时发生。

这是 C++11 中唯一重要的事情。相比之下,Java 内存模型还尝试定义存在数据竞争时的行为,并且他们花了将近十年的时间才提出合理的规范。C++11 没有犯同样的错误。


更多的信息

我有点惊讶这些基础知识并不为人所知。确定的信息来源是C++11 标准中的多线程执行和数据竞争部分。但是,规范很难理解。

Hans Boehm 的演讲是一个很好的起点——例如在线视频:

还有很多其他好的资源,我在其他地方提到过,例如:

于 2012-04-20T18:55:50.110 回答
3

没有并行访问相同的数据,所以没有问题:

  • 线程 1 开始执行Obj::Obj().
  • 线程 1 完成Obj::Obj().
  • 线程 1 将对线程 2 占用的内存的引用传递obj
  • 线程 1 从不对该内存执行任何其他操作(不久之后,它陷入无限循环)。
  • 线程 2 获取对 . 占用的内存的引用obj
  • 线程 2 大概对它做了一些事情,不受线程 1 的干扰,线程 1 仍然无限循环。

唯一潜在的问题是,如果Send不充当内存屏障,那么它就不会真正成为“合法传输设备”。

于 2012-04-20T17:53:34.423 回答
2

正如其他人所暗示的那样,构造函数不是线程安全的唯一方法是在构造函数完成之前以某种方式获取指向它的指针或引用,并且唯一的发生方式是构造函数本身具有的代码注册this指向某种类型的容器的指针,该容器在线程之间共享。

现在在您的具体示例中,Branko Dimitrijevic很好地完整地解释了您的情况如何。但在一般情况下,我会说在构造函数完成之前不要使用某些东西,尽管我认为在构造函数完成之前不会发生任何“特殊”。当它进入继承链中的(最后一个)构造函数时,该对象几乎完全“可以使用”,其所有成员变量都已初始化,等等。因此并不比任何其他关键部分工作差,但另一个线程首先需要知道它,唯一的方法是如果你this以某种方式共享构造函数本身。因此,如果您是,请仅将其作为“最后一件事”。

于 2012-04-20T18:01:20.237 回答
1

如果您编写了两个线程,并且知道第一个线程没有访问它而第二个线程访问它,那么它只是安全的(有点)。例如,如果构造它的线程在传递引用/指针后从不访问它,那你就可以了。否则它是线程不安全的。您可以通过使所有访问数据成员(读或写)的方法锁定内存来改变这一点。

于 2012-04-20T17:47:11.017 回答
0

直到现在阅读这个问题......仍然会发表我的评论:

静态局部变量

当您在多线程环境中时,有一种可靠的方法来构造对象,即使用静态局部变量(静态局部变量-CppCoreGuidelines),

从上述参考:“这是与初始化顺序相关的问题的最有效解决方案之一。在多线程环境中,静态对象的初始化不会引入竞争条件(除非您不小心从其内部访问共享对象)构造函数)。”

另请注意,如果 X 的销毁涉及需要同步的操作,您可以在堆上创建对象并同步何时调用析构函数。

下面是我写的一个例子来展示Construct On First Use Idiom,这基本上就是参考所谈论的内容。

#include <iostream>
#include <thread>
#include <vector>

class ThreadConstruct
{
public:
    ThreadConstruct(int a, float b) : _a{a}, _b{b}
    {
        std::cout << "ThreadConstruct construct start" << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(2));
        std::cout << "ThreadConstruct construct end" << std::endl;
    }

    void get()
    {
        std::cout << _a << " " << _b << std::endl;
    }

private:
    int _a;
    float _b;
};


struct Factory
{
    template<class T, typename ...ARGS>
    static T& get(ARGS... args)
    {
        //thread safe object instantiation
        static T instance(std::forward<ARGS>(args)...);
        return instance;
    }
};

//thread pool
class Threads
{
public:
    Threads() 
    {
        for (size_t num_threads = 0; num_threads < 5; ++num_threads) {
            thread_pool.emplace_back(&Threads::run, this);
        }
    }

    void run()
    {
        //thread safe constructor call
        ThreadConstruct& thread_construct = Factory::get<ThreadConstruct>(5, 10.1);
        thread_construct.get();
    }

    ~Threads() 
    {
        for(auto& x : thread_pool) {
            if(x.joinable()) {
                x.join();
            }
        }
    }

private:
    std::vector<std::thread> thread_pool;
};


int main()
{
    Threads thread;

    return 0;
}

输出:

ThreadConstruct construct start
ThreadConstruct construct end
5 10.1
5 10.1
5 10.1
5 10.1
5 10.1
于 2016-11-12T00:07:06.020 回答