【问题标题】:How do I safely release an object shared between threads using boost::shared_ptr?如何使用 boost::shared_ptr 安全地释放线程之间共享的对象?
【发布时间】:2012-06-18 16:56:32
【问题描述】:

我想知道,这样实施是否安全? :

typedef shared_ptr<Foo>  FooPtr;    
FooPtr                  *gPtrToFooPtr    // global variable

// init (before any thread has been created)
void init()
{
    gPtrToFooPtr = new FooPtr(new Foo);
}

// thread A, B, C, ..., K
// Once thread Z execute read_and_drop(), 
// no more call to read() from any thread.
// But it is possible even after read_and_drop() has returned,
// some thread is still in read() function.
void read()
{
    FooPtr a = *gPtrToFooPtr;
    // do useful things (read only)
}

// thread Z (executed once)
void read_and_drop()
{
    FooPtr b = *gPtrToFooPtr;
    // do useful things with a (read only)
    b.reset();
}

我们不知道哪个线程会执行实际释放。 boost的shared_ptr在这种情况下能安全发布吗?

根据boost的文档,shared_ptr的线程安全是:

可以“读取”shared_ptr 实例(仅使用 const 访问 操作)同时由多个线程。 不同 shared_ptr 实例可以“写入”(使用可变操作访问,例如 多个线程同时作为 operator= 或 reset)。

就我而言,上面的代码没有违反我上面提到的任何线程安全标准。而且我相信代码应该可以正常运行。有人告诉我是对是错吗?

提前致谢。


于 2012-06-20 01:00 UTC+9 编辑

上面的伪代码运行良好。 shared_ptr 实现保证在多个线程访问它的实例的情况下正常工作(每个线程必须访问自己的 shared_ptr 实例,使用 复制构造函数 )。

请注意,在上面的伪代码中,您必须delete gPtrToFooPtr 才能让shared_ptr 实现最终释放(将引用计数减一)它拥有的对象(不是正确的表达式,因为它不是auto_ptr,但谁在乎 ;) )。并且在这种情况下,您必须意识到它可能会在多线程应用程序中导致 SIGSEGV。

【问题讨论】:

    标签: c++ multithreading boost thread-safety shared-ptr


    【解决方案1】:

    您如何在这里定义“安全”?如果您将其定义为“我想确保对象仅被销毁一次”,那么是的,释放是安全的。但是,问题在于您的示例中两个线程共享一个智能指针。这根本不安全。一个线程执行的reset() 可能对另一个线程不可见。

    如文档所述,智能指针提供与内置类型(即指针)相同的保证。因此,在其他线程可能仍在读取时执行无保护写入是有问题的。当另一个读取线程将看到另一个线程的写入时,它是未定义的。因此,当一个线程调用 reset() 时,指针可能不会在另一个线程中重置,因为 shared_ptr 实例本身是共享的。

    如果你想要某种线程安全,你必须使用两个共享指针实例。然后,当然,重置其中一个不会释放对象,因为另一个线程仍然具有对它的引用。通常这种行为是有意的。

    但是,我认为更大的问题是您滥用了 shared_ptrs。使用 shared_ptrs 的指针并在堆上分配 shared_ptr(使用 new)是非常罕见的。如果你这样做,你就会遇到想要避免再次使用智能指针的问题(你现在必须管理 shared_ptr 的生命周期)。不妨先看看一些关于智能指针及其用法的示例代码。

    【讨论】:

    • 那么,如何将全局 shared_ptr 的类型更改为,例如,FooPtr gFooPtr; 和分配给FooPtr a = gFooPtr; 和FooPtr b = gFooPtr;?我猜这与使用指向动态分配的 shared_ptr 实例的指针相同。
    • 而且我认为 shared_ptr 的引用计数会被 boost 提供的无锁机制保护的引用计数阻止。因为唯一可能的写入发生在释放 Foo 的实例时,并且它受到 shared_ptr 的引用计数的保护。
    • 那会更好(另请参阅我帖子中的编辑,最好再读一遍)。但是,该对象有两个引用,因此,仅重置其中一个不会释放该对象。您必须在每个线程中重置指针。这通常是智能指针的预期行为:如果一个线程仍然有一个有效的句柄,那么另一个线程可能无法删除该对象。因此,最好的办法是给每个线程一个自己的智能指针实例,并在线程完成时重置它。
    • @你的第二条评论:是的,引用计数受到保护!但这是唯一受到保护的东西。不受保护的是指针本身。即,如果您有一个由两个线程共享的智能指针实例(总是不是一个好主意),那么重置指针将破坏对象(正确的行为)但另一个线程可能看不到指针被重置(不正确的行为)。再说一遍:发布本身很好。对象被销毁。但是另一个线程可能有一个共享指针,它仍然指向被破坏的对象。
    • 好吧,@orchistro 正在使用多个 shared_ptr 实例,因为 FooPtr b = *gPtrToFooPtr; copy-constructs 在线程 Z 的堆栈上创建了一个新的 shared_ptr。调用 reset()在这个shared_ptr 上是安全的,因为引用计数受到保护。
    【解决方案2】:

    为了你好,我会诚实的。

    你的代码做了很多事情,几乎所有的事情都是无用和荒谬的。

    typedef shared_ptr<Foo>  FooPtr;    
    FooPtr                  *gPtrToFooPtr    // global variable
    

    指向智能指针的原始指针,取消了自动资源管理的优势,并没有解决任何问题。

    void read()
    {
        FooPtr a = *gPtrToFooPtr;
        // do useful things (read only)
    }
    

    a 没有以任何有意义的方式使用。

    {
        FooPtr b = ...
        b.reset();
    }
    

    b.reset() 在这里没用,b 反正快要被销毁了。 b 在这个函数中没有任何用途。

    恐怕你不知道自己在做什么,智能指针是干什么用的,shared_ptr怎么用,怎么做MT编程;所以,你最终会得到一堆荒谬的无用功能来解决问题。

    做简单的事情怎么样:

    Foo f;
    
    // called before others functions 
    void init() {
        // prepare f
    }
    
    // called in many threads {R1, R2, ... Rn} in parallel
    void read()
    {
        // use f (read-only)
    }
    
    // called after all threads {R1, R2, ... Rn} have terminated
    void read_and_drop()
    {
        // reset f
    }
    

    read_and_drop() 之前不能调用可以保证其他线程没有读取f。

    【讨论】:

    • 首先,感谢您的坦率,谢谢。其次,我意识到我的想法是完全错误的,我在写这篇文章几个小时后就按照我的方式做了,看起来很像你的建议。最后,你的语气很痛$'(
    • 我对最后一部分感到抱歉。线程间通信必须使用适当的同步原语(互斥锁、互斥锁+条件、屏障...)而不是原始指针、shared_ptr 或两者的组合来完成。其他答案未能说明这些要点,这就是为什么我觉得有必要发布这个答案。
    【解决方案3】:

    您的编辑:

    为什么不先在全局shared_ptr 上调用reset()?

    • 如果您是最后一个访问该对象的人,可以将其删除,然后您删除堆上的shared_ptr。
    • 如果其他线程仍在使用它,则将 ref 计数减一,并将全局 ptr 与指向的(仍然存在的)对象“断开连接”。然后,您可以安全地删除堆上的 shared_ptr,而不会影响任何可能仍在使用它的线程。

    【讨论】:

    • 我需要至少保留一个对象实例。
    • 我只是参考您的评论,删除全局指针的人需要小心避免 SIGSEGV。我的方法应该避免这种情况。
    • 哦,是的,通过调用 reset() 减少引用计数器将保证释放 shared_ptr 实例拥有的对象。当我提到 SIGSEGV 时,我想到了我的伪代码,其中 shared_ptr 的全局实例不在堆栈中,而是在堆中。在一天结束时,您将需要一段时间释放用于保存全局 shared_ptr 的内存区域。当你free() 时,我的意思是,要小心确保没有其他线程会尝试复制全局 shared_ptr。无论如何,由于我对我所面临的要求缺乏解释,有些事情可能看起来有点奇怪。
    • "为什么不在全局 shared_ptr 上先调用 reset()?" 没有。read() 在调用 reset() 时仍然可以执行。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多