带有类参数的 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 [重复]