【问题标题】:Probable bug in weak_ptr passed to a threadweak_ptr 中的可能错误已传递给线程
【发布时间】:2021-12-20 19:38:46
【问题描述】:

在这个代码示例中,我们将std::shared_ptr<int> 传递给一个线程,并期望它被弱化为std::weak_ptr<int>。但在 Visual Studio 2019 中,t 具有很强的参考意义,并且该程序的输出在调试和发布版本中都是“pw alive”。编译器g++ -lpthread在linux上也有同样的问题。

这个玩具示例的动机是更严重的情况,其中线程t 进行一些长时间的计算,然后通过共享资源发送结果t 作为参数接收。这些资源可能在t 的计算过程中被其他线程删除,在这种情况下t 变得无用并且可以提前终止。但是由于这个(疑似)错误,t 使资源保持活动状态并浪费内存和计算时间。

这真的是一个错误,还是我误解了 weak_ptrshared_ptr 之间的关系?如果这是一个错误,您知道在哪里报告吗?对于 Visual Studio 和 g++,可能在不同的地方。

#include <iostream>
#include <string>
#include <thread>
#include <chrono>

void TestWeakPtrAlive(std::weak_ptr<int> pw)
{
  // wait for main thread to delete pw
  std::this_thread::sleep_for(std::chrono::milliseconds(1000));
  std::shared_ptr<int> p = pw.lock();
  std::cout << (p ? "pw alive" : "pw dead")
            << std::endl;
}

int main()
{
  std::shared_ptr<int> p = std::make_shared<int>(18);
  std::thread t(TestWeakPtrAlive, p);
  p = nullptr;
  t.join();
  return 0;
}

【问题讨论】:

  • 所以,你的问题是,“我尝试过的每个实现都做同样的事情,我认为这不是它应该做的,所以它们不都是错误的吗?”
  • 按预期工作。线程通过值获取p,存储它,然后线程函数开始获取基于存储的p 值创建的weak_ptr。您有两个shared_ptr 实例,一个在main 函数中,第二个在线程中,所以weak_ptr 必须处于活动状态(通过p = nullptr,只发布了一个,对象仍然活动,引用计数器为1)。
  • Re:“应该够长”——你不能通过猜测可能的情况来编写线程安全的代码。你的直觉几乎总是错误的。 p = nullptr; 周围没有同步,所以程序的行为是未定义的。最糟糕的是,当你测试代码时它会“正常”工作,而当你为最重要的客户演示它时它会失败。
  • @V.Semeria -- 慢点。你并不像你想象的那么了解这件事。 std::thread 构造函数只是复制它的参数;它不会通过查看线程函数来决定它期望的类型。在设置新线程的代码中,调用线程函数使用传递给线程构造函数的参数的副本。所以,是的,如果类型不可转换,那就是错误。不是因为参数不能传递给构造函数;他们可以而且他们是;但因为在构造函数内部无法转换对象。
  • 一个很大的危险领域是通过引用来论证。在单个线程中,该参数的值在函数运行时不会更改,除非被调用的函数更改它。在线程函数中,构造函数返回后,创建线程可以修改该值,使该属性消失。复制该值可确保没有任何此类意外隐藏在表面之下。在线程之间共享一个对象需要做一些额外的事情,比如std::ref(p),实际上是通过引用传递。

标签: c++ multithreading visual-c++ g++


【解决方案1】:

那些实现复制shared_ptr。即他们复制参数并将其绑定到线程的生命周期。复制是用于创建线程的参数绑定的默认设置。

除非标准规定实现不能这样做,否则这不会被视为错误。

就获得你想要的行为而言,我认为如果你使用 lambda,你会遇到同样的问题。相反,您可以使用仿函数,或者您可以使用一些全局或静态变量。

这是您转换为使用仿函数的示例。好处是您可以控制成员变量。缺点是样板。

#include <iostream>
#include <string>
#include <thread>
#include <chrono>

class TestWeakPtrAlive
{
    std::weak_ptr<int> m_pw;

public:
    TestWeakPtrAlive(std::weak_ptr<int> pw)
        : m_pw(std::move(pw))
    {}
  
    void operator()()
    {
        // wait for main thread to delete pw
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::shared_ptr<int> p = m_pw.lock();
        std::cout << (p ? "pw alive" : "pw dead")
                  << std::endl;
    }
};

int main()
{
    std::shared_ptr<int> p = std::make_shared<int>(18);
    auto functor = TestWeakPtrAlive(p);
    std::thread t(std::move(functor));
    p = nullptr;
    t.join();
    return 0;
}

编辑:我只是想到了一个相当明显的解决方案,即创建一个weak_ptr并将其传递给线程构造函数。

#include <iostream>
#include <string>
#include <thread>
#include <chrono>

void TestWeakPtrAlive(std::weak_ptr<int> pw)
{
    // wait for main thread to delete pw
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    std::shared_ptr<int> p = pw.lock();
    std::cout << (p ? "pw alive" : "pw dead")
        << std::endl;
}

int main()
{
    std::shared_ptr<int> p = std::make_shared<int>(18);
    std::weak_ptr<int> wp = p;
    std::thread t(TestWeakPtrAlive, std::move(wp));
    p = nullptr;
    t.join();
    return 0;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-09-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-08
    • 2019-05-05
    • 2023-03-05
    • 1970-01-01
    相关资源
    最近更新 更多