繁体   English   中英

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

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

源自此问题与此问题相关

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

// thread 1
Obj obj;

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

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

或者:是否有任何合法的方法在线程之间传递数据,这些方法不会强制执行与线程触及的其他内容有关的内存排序? 从硬件的角度来看,我认为没有任何理由不可能。

澄清; 问题是关于缓存一致性,内存排序等等。 在线程2的内存视图包含构造obj涉及的写入之前,线程2可以获取并使用指针吗? 错过引用Alexandrescu(?) “一个恶意的CPU设计师和编译器编写者是否可以共同构建一个标准的符合系统来实现这一目标?”

关于线程安全的推理可能很困难,而且我不是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 Draft):“程序的执行包含数据竞争,如果它在不同的线程中包含两个冲突的动作,其中至少有一个不是原子的 ,也不是发生在另一个之前 。任何此类数据竞争导致未定义的行为 。“ 之前发生的术语是在同一文档中定义的。

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

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


更多信息

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

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

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

没有对相同数据的并行访问,因此没有问题:

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

唯一可能的问题是,如果Send不是一个内存屏障,那么它就不会真正成为“合法转移设备”。

正如其他人所提到的,构造函数不是线程安全的唯一方法是在构造函数完成之前以某种方式获取指针或对它的引用,并且唯一的方法是如果构造函数本身具有代码将this指针注册到跨线程共享的某种类型的容器。

现在在你的具体例子中, Branko Dimitrijevic对你的案件如何处理做了很好的完整解释。 但是在一般情况下,我会说在构造函数完成之前不要使用某些东西,尽管我认为在构造函数完成之前没有任何“特殊”。 当它进入继承链中的(最后一个)构造函数时,该对象几乎完全“好”,其所有成员变量都被初始化,等等。所以没有比其他任何关键部分工作更糟糕,但是另一个线程需要知道关于它的第一次,出现这种情况的唯一途径是,如果你分享this在构造函数本身莫名其妙。 所以,如果你是“最后一件事”,那么这只是做到了。

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

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

静态局部变量

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

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

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

下面是我写的一个例子,用于显示构造初次使用成语 ,这基本上是参考文献所述。

#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

暂无
暂无

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

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