简体   繁体   English

构造函数在C ++和/或C ++ 11中是否安全?

[英]Are constructors thread safe in C++ and/or C++11?

Derived from this question and related to this question : 源自此问题与此问题相关

If I construct an object in one thread and then convey a reference/pointer to it to another thread, is it thread un-safe for that other thread to access the object without explicit locking/memory-barriers? 如果我在一个线程中构造一个对象, 然后将一个引用/指针传递给另一个线程,那么该线程的其他线程在没有显式锁定/内存屏障的情况下访问该对象是不安全的吗?

// thread 1
Obj obj;

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

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

Alternatively: is there any legal way to convey data between threads that doesn't enforce memory ordering with regards to everything else the thread has touched? 或者:是否有任何合法的方法在线程之间传递数据,这些方法不会强制执行与线程触及的其他内容有关的内存排序? From a hardware standpoint I don't see any reason it shouldn't be possible. 从硬件的角度来看,我认为没有任何理由不可能。

To clarify; 澄清; the question is with regards to cache coherency, memory ordering and whatnot. 问题是关于缓存一致性,内存排序等等。 Can Thread 2 get and use the pointer before Thread 2's view of memory includes the writes involved in constructing obj ? 在线程2的内存视图包含构造obj涉及的写入之前,线程2可以获取并使用指针吗? To miss-quote Alexandrescu(?) "Could a malicious CPU designer and compiler writer collude to build a standard conforming system that make that break?" 错过引用Alexandrescu(?) “一个恶意的CPU设计师和编译器编写者是否可以共同构建一个标准的符合系统来实现这一目标?”

Reasoning about thread-safety can be difficult, and I am no expert on the C++11 memory model. 关于线程安全的推理可能很困难,而且我不是C ++ 11内存模型的专家。 Fortunately, however, your example is very simple. 但幸运的是,你的例子非常简单。 I rewrite the example, because the constructor is irrelevant. 我重写了这个例子,因为构造函数是无关紧要的。

Simplified Example 简化示例

Question: Is the following code correct? 问题:以下代码是否正确? Or can the execution result in undefined behavior ? 或者执行会导致未定义的行为吗?

// 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: There may be a data race on the memory location of answer , and thus the execution results in undefined behavior . 答: answer的内存位置可能存在数据争用,因此执行会导致未定义的行为 See below for details. 请参阅下文了解详情。


Implementation of Data Transfer 实施数据传输

Of course, the answer depends on the possible and legal implementations of the functions send and receive . 当然,答案取决于函数sendreceive的可能和合法实现。 I use the following data-race-free implementation. 我使用以下数据无竞争实现。 Note that only a single atomic variable is used, and all memory operations use std::memory_order_relaxed . 请注意,仅使用单个原子变量,并且所有内存操作都使用std::memory_order_relaxed Basically this means, that these functions do not restrict memory re-orderings. 基本上这意味着,这些功能不会限制内存重新排序。

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

Order of Memory Operations 内存操作顺序

On multicore systems, a thread can see memory changes in a different order as what other threads see. 在多核系统上,线程可以按照其他线程看到的不同顺序查看内存更改。 In addition, both compilers and CPUs may reorder memory operations within a single thread for efficiency - and they do this all the time. 此外,编译器和CPU都可以在单个线程内重新排序内存操作以提高效率 - 而且他们一直这样做。 Atomic operations with std::memory_order_relaxed do not participate in any synchronization and do not impose any ordering. 使用std::memory_order_relaxed原子操作不参与任何同步,也不会强加任何排序。

In the above example, the compiler is allowed to reorder the operations of thread B, and execute B2 before B1, because the reordering has no effect on the thread itself. 在上面的例子中,允许编译器重新排序线程B的操作,并在B1之前执行B2,因为重新排序对线程本身没有影响。

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

Data Race 数据竞赛

C++11 defines a data race as follows (N3290 C++11 Draft): "The execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic , and neither happens before the other. Any such data race results in undefined behavior ." C ++ 11定义了一个数据竞争如下(N3290 C ++ 11 Draft):“程序的执行包含数据竞争,如果它在不同的线程中包含两个冲突的动作,其中至少有一个不是原子的 ,也不是发生在另一个之前 。任何此类数据竞争导致未定义的行为 。“ And the term happens before is defined earlier in the same document. 之前发生的术语是在同一文档中定义的。

In the above example, B1 and A2 are conflicting and non-atomic operations, and neither happens before the other. 在上面的例子中,B1和A2是冲突和非原子操作,并且都不会发生在另一个之前。 This is obvious, because I have shown in the previous section, that both can happen at the same time. 这很明显,因为我在上一节中已经表明,两者都可以同时发生。

That's the only thing that matters in C++11. 这是C ++ 11中唯一重要的事情。 In contrast, the Java Memory Model also tries to define the behavior if there are data races, and it took them almost a decade to come up with a reasonable specification. 相比之下,如果存在数据竞争,Java内存模型也会尝试定义行为,并且花了将近十年的时间才能得出合理的规范。 C++11 didn't make the same mistake. C ++ 11没有犯同样的错误。


Further Information 更多信息

I'm a bit surprised that these basics are not well known. 我对这些基础知识并不为人所知感到有些惊讶。 The definitive source of information is the section Multi-threaded executions and data races in the C++11 standard. 确切的信息来源是C ++ 11标准中的多线程执行和数据争用部分。 However, the specification is difficult to understand. 但是,规范很难理解。

A good starting point are Hans Boehm's talks - eg available as online videos: 一个很好的起点是Hans Boehm的演讲 - 例如在线视频:

There are also a lot of other good resources, I have mentioned elsewhere, eg: 还有很多其他好的资源,我在其他地方提到过,例如:

There is no parallel access to the same data, so there is no problem: 没有对相同数据的并行访问,因此没有问题:

  • Thread 1 starts execution of Obj::Obj() . 线程1开始执行Obj::Obj()
  • Thread 1 finishes execution of Obj::Obj() . 线程1完成Obj::Obj()
  • Thread 1 passes reference to the memory occupied by obj to thread 2. 线程1将对obj占用的内存的引用传递给线程2。
  • Thread 1 never does anything else with that memory (soon after, it falls into infinite loop). 线程1从不对该内存做任何其他事情(不久之后,它会陷入无限循环)。
  • Thread 2 picks-up the reference to memory occupied by obj . 线程2获取对obj占用的内存的引用。
  • Thread 2 presumably does something with it, undisturbed by thread 1 which is still infinitely looping. 线程2可能与它做了一些事情,不受线程1的干扰,线程1仍然无限循环。

The only potential problem is if Send didn't acts as a memory barrier, but then it wouldn't really be a "legal transfer device". 唯一可能的问题是,如果Send不是一个内存屏障,那么它就不会真正成为“合法转移设备”。

As others have alluded to, the only way in which a constructor is not thread-safe is if something somehow gets a pointer or reference to it before the constructor is finished, and the only way that would occur is if the constructor itself has code that registers the this pointer to some type of container which is shared across threads. 正如其他人所提到的,构造函数不是线程安全的唯一方法是在构造函数完成之前以某种方式获取指针或对它的引用,并且唯一的方法是如果构造函数本身具有代码将this指针注册到跨线程共享的某种类型的容器。

Now in your specific example, Branko Dimitrijevic gave a good complete explanation how your case is fine. 现在在你的具体例子中, Branko Dimitrijevic对你的案件如何处理做了很好的完整解释。 But in the general case, I'd say to not use something until the constructor is finished, though I don't think there's anything "special" that doesn't happen until the constructor is finished. 但是在一般情况下,我会说在构造函数完成之前不要使用某些东西,尽管我认为在构造函数完成之前没有任何“特殊”。 By the time it enters the (last) constructor in an inheritance chain, the object is pretty much fully "good to go" with all of its member variables being initialized, etc. So no worse than any other critical section work, but another thread would need to know about it first, and the only way that happens is if you're sharing this in the constructor itself somehow. 当它进入继承链中的(最后一个)构造函数时,该对象几乎完全“好”,其所有成员变量都被初始化,等等。所以没有比其他任何关键部分工作更糟糕,但是另一个线程需要知道关于它的第一次,出现这种情况的唯一途径是,如果你分享this在构造函数本身莫名其妙。 So only do that as the "last thing" if you are. 所以,如果你是“最后一件事”,那么这只是做到了。

It is only safe (sort of) if you wrote both threads, and know the first thread is not accessing it while the second thread is. 如果你编写了两个线程,并且知道第一个线程在第二个线程没有访问它时,它是安全的(有点)。 For example, if the thread constructing it never accesses it after passing the reference/pointer, you would be OK. 例如,如果构造它的线程在传递引用/指针后从不访问它,那么你就可以了。 Otherwise it is thread unsafe. 否则它是线程不安全的。 You could change that by making all methods that access data members (read or write) lock memory. 您可以通过使访问数据成员(读取或写入)的所有方法锁定内存来更改它。

Read this question until now... Still will post my comments: 直到现在才读到这个问题...仍然会发表我的评论:

Static Local Variable 静态局部变量

There is a reliable way to construct objects when you are in a multi-thread environment, that is using a static local variable ( static local variable-CppCoreGuidelines ), 当您处于多线程环境中时,有一种可靠的方法来构造对象,即使用静态局部变量( 静态局部变量-CppCoreGuidelines ),

From the above reference: "This is one of the most effective solutions to problems related to initialization order. In a multi-threaded environment the initialization of the static object does not introduce a race condition (unless you carelessly access a shared object from within its constructor)." 从上面的引用:“这是与初始化顺序相关的问题最有效的解决方案之一。在多线程环境中,静态对象的初始化不会引入竞争条件(除非你不小心从其内部访问共享对象)构造函数)。”

Also note from the reference, if the destruction of X involves an operation that needs to be synchronized you can create the object on the heap and synchronize when to call the destructor. 另请注意,如果X的销毁涉及需要同步的操作,则可以在堆上创建对象并同步何时调用析构函数。

Below is an example I wrote to show the Construct On First Use Idiom , which is basically what the reference talks about. 下面是我写的一个例子,用于显示构造初次使用成语 ,这基本上是参考文献所述。

#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;
}

Output: 输出:

ThreadConstruct construct start
ThreadConstruct construct end
5 10.1
5 10.1
5 10.1
5 10.1
5 10.1

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM