【问题标题】:Using a shared pointer in a std::thread在 std::thread 中使用共享指针
【发布时间】:2015-05-05 08:37:50
【问题描述】:

我有一个场景:

  1. 我从一个 dll 中启动了一个新线程,该线程执行了一些工作。

  2. 可以在新线程完成工作之前调用 dll 析构函数。

  3. 如果是这样,我想在析构函数中设置一个布尔标志来告诉线程返回而不是继续。

如果我尝试以下操作,我发现因为调用了析构函数并且 MyDll 超出了范围,然后 m_cancel 被删除并且它的值不可靠(有时为假,有时为真)所以我不能使用这个方法。

方法一

//member variable declared in header file
bool m_cancel = false;
MyDll:~MyDll()
{
    m_cancel = true;
}

//Function to start receiving data asynchronously  
void MyDll::GetDataSync()
{
    std::thread([&]() 
    {
        SomeFunctionThatCouldTakeAWhile();

        if( m_cancel == true )
          return;

        SomeFunctionThatDoesSomethingElse();    
    }
}

所以我查看了这个示例Replacing std::async with own version but where should std::promise live?,其中使用了可以从两个线程访问的共享指针。

所以我想我应该:

  1. 创建一个指向 bool 的共享指针并将其传递给我启动的新线程。

  2. 在析构函数中,改变这个共享指针的值,并在新线程中检查。

这是我想出的,但我不确定这是否是解决此问题的正确方法。

方法二

//member variable declared in header file
std::shared_ptr<bool> m_Cancel;

//Constructor
 MyDll:MyDll()
{
    m_Cancel = make_shared<bool>(false);
}
//Destructor
MyDll:~MyDll()
{
    std::shared_ptr<bool> m_cancelTrue = make_shared<bool>(true);

    m_Cancel = std::move(m_cancelTrue);
}

//Function to start receiving data asynchronously  
void MyDll::GetDataSync()
{
    std::thread([&]() 
    {
        SomeFunctionThatCouldTakeAWhile();

        if( *m_Cancel.get() == true )
            return;

        SomeFunctionThatDoesSomethingElse();    
    }
}

如果我执行上述操作,那么 if( *m_Cancel.get() == true ) 会导致崩溃(访问冲突)

  1. 我是通过值还是通过引用 std::thread 来传递共享指针?

  2. 因为它是一个共享指针,即使 MyDll 超出范围,std::thread 所拥有的副本仍然有效吗??

我该怎么做??

方法3

//Members declared in header file
std::shared_ptr<std::atomic<bool>> m_Cancel;

//Constructor
 MyDll:MyDll()
{
    //Initialise m_Cancel to false
    m_Cancel = make_shared<std::atomic<bool>>(false);
}
//Destructor
MyDll:~MyDll()
{
    //Set m_Cancel to true
    std::shared_ptr<std::atomic<bool>> m_cancelTrue = make_shared<std::atomic<bool>>(true);

    m_Cancel = std::move(m_cancelTrue);
}

//Function to start receiving data asynchronously  
void MyDll::GetDataSync()
{
    std::thread([=]() //Pass variables by value
    {
        SomeFunctionThatCouldTakeAWhile();

        if( *m_Cancel.get() == true )
            return;

        SomeFunctionThatDoesSomethingElse();    
    }
}

我资助的是,当调用析构函数然后调用 if( *m_Cancel.get() == true ) 时,它总是崩溃。

我是不是做错了什么?

解决方案

我添加了一个互斥锁,以防止在新线程中检查取消后 dtor 返回。

//Members declared in header file
std::shared_ptr<std::atomic<bool>> m_Cancel;
std::shared_ptr<std::mutex> m_sharedMutex;

//Constructor
 MyDll:MyDll()
{
    //Initialise m_Cancel to false
    m_Cancel = make_shared<std::atomic<bool>>(false);
    m_sharedMutex = make_shared<std::mutex>();
}
//Destructor
MyDll:~MyDll()
{
    //Set m_Cancel to true
    std::shared_ptr<std::atomic<bool>> m_cancelTrue = make_shared<std::atomic<bool>>(true);

    std::lock_guard<std::mutex> lock(*m_sharedMutex);//lock access to m_Cancel
    {
        *m_Cancel = std::move(cancelTrue);
    }
}

//Function to start receiving data asynchronously  
void MyDll::GetDataSync()
{
    auto cancel = this->m_Cancel;
    auto mutex = this->m_sharedMutex;

    std::thread([=]() //Pass variables by value
    {
        SomeFunctionThatCouldTakeAWhile();

        std::lock_guard<std::mutex> lock(*mutex);//lock access to cancel
        {
            if( *cancel.get() == true )
                return;

            SomeFunctionThatDoesSomethingElse();    
        }
    }
}

【问题讨论】:

  • 你能在你的析构函数中等待线程结束吗?
  • 如果我等待,那么有一个 UI 将被阻止长达 20 秒。我想不惜一切代价避免这种情况。
  • 您如何在您的MyDll 中使用std::atomic&lt;bool&gt; flag 来进行通信以中断线程。然后线程的函数会定期检查这个flag,如果它设置为真则返回。在MyDll 的析构函数中,您将flag 设置为true,然后将join() 您的线程(等待其终止)设置为不再需要很长时间,因为您发送了中断信号。
  • 我面临的挑战是我的函数 SomeFunctionThatCouldTakeAWhile 是 Curl 同步 curl_easy_perform 操作,无法取消此操作。唯一的选择是让它超时。如果我等待这个超时,那么它太长了,这就是我想开火并忘记的原因。如果我 join() 那么它会导致延迟。
  • 是否有任何我可以使用的方法/设计让 MyDll 超出范围并让我的 std::thread 检测到这一点并简单地返回而不继续?

标签: c++ multithreading pointers c++11


【解决方案1】:

第 2 步是错误的。这是设计错误。

您的第一个机制不起作用的原因很简单。 m_cancel==false 可能会被编译器优化掉。当析构函数返回时,m_cancel 将不复存在,析构函数中的任何语句都不会依赖于该写入。析构函数返回后,访问之前持有m_cancel的内存将是Undefined Behavior。

第二种机制(全局)由于更复杂的原因而失败。有一个明显的问题是您只有一个全局m_Cancel(顺便说一句,m_ 对于不是成员的东西来说是一个真正具有误导性的前缀)。但是假设您只有一个MyDll,它仍然可能由于线程原因而失败。你想要的不是shared_ptr,而是std::atomic&lt;bool&gt;。多线程访问是安全的

[编辑] 您的第三种机制失败了,因为[=] 从封闭范围中捕获了名称。 m_Cancel 不在该范围内,但 this 是。但是,您不希望该线程有this 的副本,因为this 将被销毁。解决方案:auto cancel = this-&gt;m_Cancel; std::thread([cancel](...

[编辑 2] 我认为你真的应该阅读基础知识。在版本 3 的 dtor 中,您确实更改了 m_cancel 的值。也就是说,你改变了指针。您应该更改 *m_cancel,即它所指向的内容。正如我上面指出的,线程有一个指针的副本。如果更改原始指针,线程将继续指向旧值。 (这与智能指针无关,哑指针的行为相同)。

【讨论】:

  • 如果我以同样的方式使用 std::atomic ,当调用 MyDlls 析构函数时它是否仍然超出范围??
  • @HarryBoy:如果m_cancel 顾名思义是成员(但与您的示例代码不同),那么它将超出范围。然后你需要一个shared_ptr&lt;atomic&lt;bool&gt;&gt;
  • 抱歉,是的,它是一个成员,我应该在我的代码中更好地解释它。因此,即使它超出范围并且我使用了 shared_ptr> ,std::thread 仍然有我可以访问的有效副本吗?我是否需要通过值 [=] 或引用 [&] 将其传递给 std::thread??
  • @HarryBoy:线程需要它自己的shared_ptr 副本来保持atomic&lt;bool&gt; 活着。这第二个指针将比 dtor 寿命更长。在设置m_cancel 之后,另一种方法是join() 您在dtor 中的线程。这意味着 dtor 将等待线程退出,因此 m_cancel 只有在线程完成后才会被销毁。
  • MSalters 你能看到我的编辑吗?感谢您在这方面的帮助。
猜你喜欢
  • 1970-01-01
  • 2020-07-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-10-09
  • 2018-04-14
  • 1970-01-01
  • 2017-12-03
相关资源
最近更新 更多