带有可可对象的线程中产卵线程

Spawning threads in a thread with callable object

本文关键字:线程 可可 对象      更新时间:2023-10-16

我已经多次看到了这个问题,而且看来它发生在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;
}

但是,结果代码没有意义。产卵线与时循环竞争。循环减少索引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))时,这两件事将使用模板魔术在内部发生:

  1. 使用您的可呼叫和所有args创建一个元组。
    std::tuple<mycallablerefarg, mynoncopy> thetuple(callable, std::move(thenoncopyableinstance));
    在这种情况下将复制可呼叫。

  2. std::invoke()用于调用可可,使用移动语义从元组传递给它。
    std::invoke(std::move(std::get<0>(thetuple)), std::move(std::get<1>(thetuple)));

由于使用了移动语义,因此可以期望可召唤作为参数接收RVALUE参考(在我们的情况下为mynoncopy&&(。这将我们限制为以下参数签名:

  1. mynoncopy&&
  2. const mynoncopy&
  3. T&&其中t是模板参数
  4. mynoncopy不是参考(这将称为Move-Constructor(

这些是使用不同类型的可可的汇编结果:

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.