在 C++ 线程中,我应该通过值还是引用传递 shared_ptr?

时间:2021-03-03 10:06:41

标签: c++

微软在 Thread Safety 上的这个页面说,即使有多个副本共享同一个对象,也应该使用 shared_ptr

那么这是否意味着以下两种情况都可以接受?我已经尝试了两者,它们似乎工作正常。

EDIT:实际的业务目标是从长时间运行的线程到主线程获取字符串更新。我想我应该使用 shared_ptr 因为 string 不是线程安全的。老实说,不要在意所有权。

选项 1(传递参考):

auto status = std::make_shared<std::string>();
auto f = [&status]() {
    ...
  *status = "current status";
    ...
};

std::thread t{f};

while(true) {
  std::cout << *status << std::endl;
  std::this_thread::sleep_for(1000ms);
  if (*status == "completed") break;
}

t.join();

选项 2(制作副本):

auto status = std::make_shared<std::string>();
auto f = [](std::shared_ptr<std::string> s) {
    ...
  *s= "current status";
    ...
};

std::thread t{f, status};

while(true) {
  std::cout << *status << std::endl;
  std::this_thread::sleep_for(1000ms);
  if (*status == "completed") break;
}

t.join();

EDIT2: 显然,这两种方法对于我想要实现的目标都是错误的。我需要使用 std::mutex (cppreference) 而不是使用 shared_ptr。请参阅this answer的后半部分。

4 个答案:

答案 0 :(得分:11)

通常,线程可能会在创建它们的范围内存活。在这种情况下,任何通过引用捕获的局部变量都可能在线程仍在运行时被销毁。如果是这种情况,则不应通过引用捕获。

此外,在一个线程中修改共享指针对象并在没有同步的情况下访问另一个线程会导致未定义的行为。如果这就是您所做的,那么您应该使用 std::atomic_load/atomic_store 函数访问指针,或者简单地将指针复制到每个线程中。请注意,您可以通过副本捕获:

auto f = [status]() {

此外,共享指针没有提供额外的线程安全来访问指向的对象,除了保持所有权处于活动状态并确保它被删除一次。如果指向的类型不是原子的,那么在一个线程中修改它并在没有同步的情况下在另一个线程中访问会导致未定义的行为。如果这就是你正在做的,你需要使用互斥锁或类似的东西。或者将指向的对象本身复制到每个线程中。

关于编辑后的问题:您的示例适用于最后一个案例。他们都有未定义的行为。您需要同步。

答案 1 :(得分:7)

通过引用接受 shared_ptr 很奇怪,因为您一开始就失去了使用 shared_ptr 的全部意义。您可以只使用原始指针。

在某些情况下,通过引用接受 shared_ptr 是合法的,但是如果您将它的引用提供给线程,那么一旦 shared_ptr 的实例被销毁并且线程仍然存在,它将导致 UB使用 shared_ptr

shared_ptr 的主要目的是管理对象的生命周期。如果您将它的引用传递给线程,那么您就抛弃了 shared_ptr 的全部目的和优点。

答案 2 :(得分:0)

如果使用引用,则无法分离线程。

比如这个程序会崩溃:

#include <thread>
#include <chrono>
#include <string>
#include <iostream>
void f1()
{
  auto status = std::make_shared<std::string>();
  auto f = [&status]() 
  {
     std::this_thread::sleep_for(std::chrono::seconds(1)); 
    *status = "current status";
  };
  std::thread t{f};
  t.detach();
}
int main() { 
  f1();
  std::string status="other status";//use the frame
  std::this_thread::sleep_for(std::chrono::seconds(1)); 
  std::cout<<status<<std::endl; //check the frame
}

答案 3 :(得分:0)

当您传递对 status lambda 的引用时,这意味着您需要确保 lambda 不会超过 status 变量。

想象一下,我们希望将线程创建移动到一个单独的函数中:

std::thread
spawn_thread( std::shared_ptr< std::string > status )
{
    auto f = [&status]( ) {
        // Adding a sleep to make sure this gets executed after we exit spawn_thread function.
        std::this_thread::sleep_for( std::chrono::milliseconds( 500 ) );
        *status = "current status";
    };

    std::thread t{f};
    return t;
}

int
main( )
{
    auto status = std::make_shared< std::string >( );
    auto thread = spawn_thread( status );

    while ( true )
    {
        std::cout << *status << std::endl;
        std::this_thread::sleep_for( std::chrono::milliseconds( 1000 ) );
        if ( *status == "completed" )
        {
            break;
        }
    }

    thread.join( );
}

运行这段代码很可能会导致崩溃,因为变量 status(不是它背后的共享数据)在我们在 lambda 中访问它之前就超出了范围。

当然,我们可以将 status 的引用传递给 spawn_thread 函数,但随后我们会进一步传播该问题 - 现在 spawn_thread 的调用者需要确保此变量比线程存活时间更长。< /p>

std::shared_ptr 设计用于以下情况:您不想手动控制传递的对象的生命周期,但为了使其工作,您需要按值传递它,以便内部机制保持计数shared_ptr 个实例的数量。

请记住,虽然传递和复制 shared_ptr 是线程安全的,但对存储在其中的值的并发读取和写入不是。

相关问题