带有类参数的 std::thread 初始化导致类对象被多次复制

Posted

技术标签:

【中文标题】带有类参数的 std::thread 初始化导致类对象被多次复制【英文标题】:std::thread initialization with class argument results with class object being copied multiple times 【发布时间】:2015-12-13 18:51:00 【问题描述】:

似乎如果你创建一个类的对象,并将它传递给 std::thread 初始化构造函数,那么类对象的构造和销毁总共多达 4 次。我的问题是:你能逐步解释这个程序的输出吗?为什么类在这个过程中被构造、复制构造和销毁这么多次?

示例程序:

#include <iostream>  
#include <cstdlib>
#include <ctime>
#include <thread>

class sampleClass 
public:
    int x = rand() % 100;
    sampleClass() std::cout << "constructor called, x=" << x <<     std::endl;
    sampleClass(const sampleClass &SC) std::cout << "copy constructor called, x=" << x << std::endl;
    ~sampleClass() std::cout << "destructor called, x=" << x << std::endl;
    void add_to_x() x += rand() % 3;
;

void sampleThread(sampleClass SC) 
    for (int i = 0; i < 1e8; ++i)  //give the thread something to do
        SC.add_to_x();
    
    std::cout << "thread finished, x=" << SC.x << std::endl;


int main(int argc, char *argv[]) 
    srand (time(NULL));
    sampleClass SC;
    std::thread t1 (sampleThread, SC);
    std::cout << "thread spawned" << std::endl;
    t1.join();
    std::cout << "thread joined" << std::endl;
    return 0;

输出是:

constructor called, x=92
copy constructor called, x=36
copy constructor called, x=61
destructor called, x=36
thread spawned
copy constructor called, x=62
thread finished, x=100009889
destructor called, x=100009889
destructor called, x=61
thread joined
destructor called, x=92

用 gcc 4.9.2 编译,没有优化。

【问题讨论】:

我编辑了这个例子,int x 被初始化为 rand()%100 所以你可以看到什么时候构造/销毁了哪个对象 据我所见,您构造了一次对象,然后将其传递给线程对象,线程对象在接受您的参数时再次复制构造它,然后线程对象将其传递给您的函数,因为它按值获取类,所以 copy 再次构造参数。顺便说一句,尝试添加一个移动构造函数,看看会发生什么!我想所有这些副本都会发生,因为您没有移动构造函数!查找“C++ 五法则” 另外,为什么编译时不使用优化?我确信编译器会优化掉这些冗余副本。 @adam10603 Move 似乎不是一个好的解决方案,因为在目标应用程序中我产生了几个相同类型的线程,每个线程都有一个相同类的副本(副本需要是独立的)。据我了解,移动会使原始对象处于不确定状态。至于优化 - 示例没有完全优化,因此编译器不会过多地混淆代码,无论如何即使使用-O3,输出也是相同的。 我的意思是,您将创建与线程数一样多的对象,然后将它们移动到线程中,避免复制 【参考方案1】:

在后台进行了大量的复制/移动。但请注意,调用线程构造函数时,既不会调用复制构造函数,也不会调用移动构造函数。

考虑这样的函数:

template<typename T> void foo(T&& arg);

当你有对模板参数的右值引用时,C++ 对此有点特殊。我将在这里概述规则。当您使用参数调用foo 时,参数类型将为

&& - 当参数是右值时 & - 所有其他情况

也就是说,参数将作为 r 值引用或标准引用传递。无论哪种方式,都不会调用构造函数。

现在看线程对象的构造函数:

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

此构造函数应用相同的语法,因此参数永远不会被复制/移动到构造函数参数中。

以下代码包含一个示例。

#include <iostream>
#include <thread>

class Foo
public:
    int id;

    Foo()
    
        id = 1;
        std::cout << "Default constructor, id = " << id << std::endl;
    

    Foo(const Foo& f)
    
        id = f.id + 1;
        std::cout << "Copy constructor, id = " << id << std::endl;
    

    Foo(Foo&& f)
    
        id = f.id;
        std::cout << "Move constructor, id = " << id << std::endl;
    
;

void doNothing(Foo f)

    std::cout << "doNothing\n";


template<typename T>
void test(T&& arg)



int main()

    Foo f; // Default constructor is called

    test(f); // Note here that we see no prints from copy/move constructors

    std::cout << "About to create thread object\n";
    std::thread tdoNothing, f;
    t.join();

    return 0;

这段代码的输出是

Default constructor, iCount = 1
About to create thread object
Copy constructor, id = 2
Move constructor, id = 2
Move constructor, id = 2
doNothing
首先,创建对象。 我们调用测试函数只是为了看看没有任何反应,没有构造函数调用。 因为我们将左值传递给线程构造函数,所以参数具有类型左值引用,因此对象被复制(使用复制构造函数)到线程对象中。 对象被移动到底层线程(由线程对象管理) 对象最终被移动到线程函数doNothing的参数中

【讨论】:

std::thread 在其正文中执行传递参数的内部副本。 是的,这就是我所说的:对象被复制(使用复制构造函数)到线程对象中。【参考方案2】:
int main(int argc, char *argv[]) 
    sampleClass SC; // default constructor
    std::thread t1 (sampleThread, SC); // Two copies inside thread constructor,
                                       //use std::ref(SC) to avoit it
    //..


void sampleThread(sampleClass SC)  // copy SC: pass by ref to avoid it
                                // but then modifications are for original and not the copy
  // ...

Fixed version Demo

【讨论】:

不幸的是,我的应用程序 std::ref 不是一个选项,因为我正在启动更多相同类型的线程,在我这样做之前,有很多工作来设置正在传递的类,所以我故意想复制构造它。但是我仍然不清楚,为什么 std::thread t1 (sampleThread, SC); 创建 2 个副本? @MarcinL:en.cppreference.com/w/cpp/thread/thread/thread,参见3):decay_copy 在返回中执行一个复制构造(因为在您的情况下没有移动),而在另一个调用它的位置。我认为某些实现可能会将副本数量减少到一个。 如果你想用一个巨大的类的独立副本生成多个线程(由于其他原因需要提前创建),那么是否可以避免内存泄漏?如果是故意的,创建了 2 个副本而不是 1 个,那么没有解决方案,我错过了什么吗? @MarcinL:目前没有内存泄漏。您可能有一个移动构造函数,它可能比副本便宜。否则,您可以将 sampleThread 更改为通过 const 引用并在 sampleThread 或创建线程之前进行复制(使用 std::vector...)。 @MarcinL :您似乎在这里有一些基本的误解。为什么您认为拥有一个对象的两个副本会泄漏内存?

以上是关于带有类参数的 std::thread 初始化导致类对象被多次复制的主要内容,如果未能解决你的问题,请参考以下文章

std::thread创建线程,使用std::ref()传递类对象参数

std::thread创建线程,使用std::ref()传递类对象参数

尝试在类构造函数中初始化 std::thread 时出现编译器错误 C2064 [重复]

thread::hardware_concurrency() 作为模板参数

std::thread

C++ 多线程std::thread 详解