在具有可调用对象的线程中生成线程
Spawning threads in a thread with callable object
这个问题我见过很多次,似乎在 Windoes(visual studio) 和 Linux(gcc) 中都会出现。
这是它的简化版本:
class noncopyable
{
public:
noncopyable(int n);
~noncopyable();
noncopyable(const noncopyable&) = delete;
noncopyable& operator=(const noncopyable&) = delete;
noncopyable(noncopyable&&);
int& setvalue();
private:
int* number;
};
class thread_starter
{
public:
template<typename callable>
bool start(callable & o);
};
template<typename callable>
inline bool thread_starter::start(callable & o)
{
std::thread t(
[&]() {
int i = 10;
while (i-- > 0)
{
noncopyable m(i);
std::thread child(o, std::move(m));
child.detach();
}
});
return true;
}
class callable
{
public:
virtual void operator()(noncopyable &m);
};
void callable::operator()(noncopyable & m) { m.setvalue()++; }
int main()
{
thread_starter ts;
callable o;
ts.start(o);
}
代码看起来足够合法,但无法编译。
在Visual Studio中,它会给出:
error C2893: Failed to specialize function template 'unknown-type std::invoke(_Callable &&,_Types &&...) noexcept(<expr>)'
在 GCC 中,它将给出:
error: no type named ‘type’ in ‘class std::result_of<callable(int)>’....
我想我知道问题出在某种形式的复制或引用机制上,但所有语法似乎都是正确的。
我错过了什么?
我稍微更改了示例,对于造成的混淆,我深表歉意。我正在尝试尽可能纯粹地重现问题,但我自己并不完全理解它。
对 std::thread
的调用创建了一个元组。元组不能用引用初始化。因此,您必须使用伪引用 std::ref(i)
来编译它并使用 int-refs int&
.
调用可调用对象
template <typename callable>
bool thread_starter::start(callable &o)
{
// Nonsense
std::thread t(
[&]() {
int i = 10;
while (i-- > 0)
{
std::thread child(o, std::ref(i));
child.detach();
}
});
return true;
}
但是,生成的代码毫无意义。生成的线程与 while 循环竞争。循环递减索引 i
,而线程试图递增它。无法保证这些事情何时会发生。增量和减量不是原子的。线程可能会在 lambda 完成后尝试增加索引。
简而言之,如果您编译它,由于多种原因,结果是未定义的行为。
你到底想做什么?
我做了一个类似的程序来弄清楚会发生什么。它是这样的:
class mynoncopy
{
public:
mynoncopy(int resource)
: resource(resource)
{
}
mynoncopy(mynoncopy&& other)
: resource(other.resource)
{
other.resource = 0;
}
mynoncopy(const mynoncopy& other) = delete;
mynoncopy& operator =(const mynoncopy& other) = delete;
public:
void useResource() {}
private:
int resource;
};
class mycallablevaluearg
{
public:
void operator ()(mynoncopy noncopyablething)
{
noncopyablething.useResource();
}
};
class mycallableconstrefarg
{
public:
void operator ()(const mynoncopy& noncopyablething)
{
//noncopyablething.useResource(); // can't do this becuase of const :(
}
};
class mycallablerefarg
{
public:
void operator ()(mynoncopy& noncopyablething)
{
noncopyablething.useResource();
}
};
class mycallablervaluerefarg
{
public:
void operator ()(mynoncopy&& noncopyablething)
{
noncopyablething.useResource();
}
};
class mycallabletemplatearg
{
public:
template<typename T>
void operator ()(T&& noncopyablething)
{
noncopyablething.useResource();
}
};
当您发出 std::thread(callable, std::move(thenoncopyableinstance))
时,这两件事将在内部使用模板魔法发生:
使用您的可调用对象和所有参数创建一个元组。
std::tuple<mycallablerefarg, mynoncopy> thetuple(callable,
std::move(thenoncopyableinstance));
在这种情况下将复制可调用对象。
std::invoke()
用于调用可调用对象,arg 使用移动语义从元组传递给它。
std::invoke(std::move(std::get<0>(thetuple)), std::move(std::get<1>(thetuple)));
因为使用了移动语义,所以它期望可调用对象接收一个右值引用作为参数(在我们的例子中是mynoncopy&&
)。这将我们限制为以下参数签名:
mynoncopy&&
const mynoncopy&
T&&
其中 T 是模板参数
mynoncopy
不是引用(这将调用移动构造函数)
这些是使用不同类型的可调用对象的编译结果:
mynoncopy testthing(1337);
std::thread t(mycallablerefarg(), std::move(testthing)); // Fails, because it can not match the arguments. This is your case.
std::thread t(mycallablevaluearg(), std::move(testthing)); // OK, because the move semantics will be used to construct it so it will basically work as your solution
std::thread t(mycallableconstrefarg(), std::move(testthing)); // OK, because the argument is const reference
std::thread t(mycallablervaluerefarg(), std::move(testthing)); // OK, because the argument is rvalue reference
std::thread t(mycallabletemplatearg(), std::move(testthing)); // OK, because template deduction kicks in and gives you noncopyablething&&
std::thread t(std::bind(mycallablerefarg(), std::move(testthing))); // OK, gives you a little bit of call overhead but works. Because bind() does not seem to use move semantics when invoking the callable
std::thread t(std::bind(mycallablevalue(), std::move(testthing))); // Fails, because bind() does not use move semantics when it invokes the callable so it will need to copy the value, which it can't.
这个问题我见过很多次,似乎在 Windoes(visual studio) 和 Linux(gcc) 中都会出现。 这是它的简化版本:
class noncopyable
{
public:
noncopyable(int n);
~noncopyable();
noncopyable(const noncopyable&) = delete;
noncopyable& operator=(const noncopyable&) = delete;
noncopyable(noncopyable&&);
int& setvalue();
private:
int* number;
};
class thread_starter
{
public:
template<typename callable>
bool start(callable & o);
};
template<typename callable>
inline bool thread_starter::start(callable & o)
{
std::thread t(
[&]() {
int i = 10;
while (i-- > 0)
{
noncopyable m(i);
std::thread child(o, std::move(m));
child.detach();
}
});
return true;
}
class callable
{
public:
virtual void operator()(noncopyable &m);
};
void callable::operator()(noncopyable & m) { m.setvalue()++; }
int main()
{
thread_starter ts;
callable o;
ts.start(o);
}
代码看起来足够合法,但无法编译。
在Visual Studio中,它会给出:
error C2893: Failed to specialize function template 'unknown-type std::invoke(_Callable &&,_Types &&...) noexcept(<expr>)'
在 GCC 中,它将给出:
error: no type named ‘type’ in ‘class std::result_of<callable(int)>’....
我想我知道问题出在某种形式的复制或引用机制上,但所有语法似乎都是正确的。
我错过了什么?
我稍微更改了示例,对于造成的混淆,我深表歉意。我正在尝试尽可能纯粹地重现问题,但我自己并不完全理解它。
对 std::thread
的调用创建了一个元组。元组不能用引用初始化。因此,您必须使用伪引用 std::ref(i)
来编译它并使用 int-refs int&
.
template <typename callable>
bool thread_starter::start(callable &o)
{
// Nonsense
std::thread t(
[&]() {
int i = 10;
while (i-- > 0)
{
std::thread child(o, std::ref(i));
child.detach();
}
});
return true;
}
但是,生成的代码毫无意义。生成的线程与 while 循环竞争。循环递减索引 i
,而线程试图递增它。无法保证这些事情何时会发生。增量和减量不是原子的。线程可能会在 lambda 完成后尝试增加索引。
简而言之,如果您编译它,由于多种原因,结果是未定义的行为。
你到底想做什么?
我做了一个类似的程序来弄清楚会发生什么。它是这样的:
class mynoncopy
{
public:
mynoncopy(int resource)
: resource(resource)
{
}
mynoncopy(mynoncopy&& other)
: resource(other.resource)
{
other.resource = 0;
}
mynoncopy(const mynoncopy& other) = delete;
mynoncopy& operator =(const mynoncopy& other) = delete;
public:
void useResource() {}
private:
int resource;
};
class mycallablevaluearg
{
public:
void operator ()(mynoncopy noncopyablething)
{
noncopyablething.useResource();
}
};
class mycallableconstrefarg
{
public:
void operator ()(const mynoncopy& noncopyablething)
{
//noncopyablething.useResource(); // can't do this becuase of const :(
}
};
class mycallablerefarg
{
public:
void operator ()(mynoncopy& noncopyablething)
{
noncopyablething.useResource();
}
};
class mycallablervaluerefarg
{
public:
void operator ()(mynoncopy&& noncopyablething)
{
noncopyablething.useResource();
}
};
class mycallabletemplatearg
{
public:
template<typename T>
void operator ()(T&& noncopyablething)
{
noncopyablething.useResource();
}
};
当您发出 std::thread(callable, std::move(thenoncopyableinstance))
时,这两件事将在内部使用模板魔法发生:
使用您的可调用对象和所有参数创建一个元组。
std::tuple<mycallablerefarg, mynoncopy> thetuple(callable, std::move(thenoncopyableinstance));
在这种情况下将复制可调用对象。std::invoke()
用于调用可调用对象,arg 使用移动语义从元组传递给它。std::invoke(std::move(std::get<0>(thetuple)), std::move(std::get<1>(thetuple)));
因为使用了移动语义,所以它期望可调用对象接收一个右值引用作为参数(在我们的例子中是mynoncopy&&
)。这将我们限制为以下参数签名:
mynoncopy&&
const mynoncopy&
T&&
其中 T 是模板参数mynoncopy
不是引用(这将调用移动构造函数)
这些是使用不同类型的可调用对象的编译结果:
mynoncopy testthing(1337);
std::thread t(mycallablerefarg(), std::move(testthing)); // Fails, because it can not match the arguments. This is your case.
std::thread t(mycallablevaluearg(), std::move(testthing)); // OK, because the move semantics will be used to construct it so it will basically work as your solution
std::thread t(mycallableconstrefarg(), std::move(testthing)); // OK, because the argument is const reference
std::thread t(mycallablervaluerefarg(), std::move(testthing)); // OK, because the argument is rvalue reference
std::thread t(mycallabletemplatearg(), std::move(testthing)); // OK, because template deduction kicks in and gives you noncopyablething&&
std::thread t(std::bind(mycallablerefarg(), std::move(testthing))); // OK, gives you a little bit of call overhead but works. Because bind() does not seem to use move semantics when invoking the callable
std::thread t(std::bind(mycallablevalue(), std::move(testthing))); // Fails, because bind() does not use move semantics when it invokes the callable so it will need to copy the value, which it can't.