使用std::unique_ptr/std::shared_ptr确认线程安全

3
我的应用程序有一个IRC模块,本质上是一个普通的客户端。由于这个模块有很多线程,我存在一种风险,即插件检索例如用户昵称-在那个时间是有效的,但解析器会触发更新,从而更改所说的昵称。一旦另一个线程再次执行,它就处理指向无效内存的指针,因为将返回+复制作为原子操作是不可能的。
基于下面的代码,我的假设是否正确?因此,我想我必须使用通常的互斥锁定/解锁方法,除非有人能够确认或建议其他方法(我宁愿不必转换并返回共享指针,但我想这是一个有效的选项,只是我打算SWIG'ing这个,不知道它是否喜欢它们)。
IrcUser.h
class IrcUser : public IrcSubject
{
private:
    ...
    std::shared_ptr<std::string>    _nickname;
    std::shared_ptr<std::string>    _ident;
    std::shared_ptr<std::string>    _hostmask;
public:
    ...
    const c8*
    Ident() const
    { return _ident.get()->c_str(); }

    const c8*
    Hostmask() const
    { return _hostmask.get()->c_str(); }

    const u16
    Modes() const
    { return _modes; }

    const c8*
    Nickname() const
    { return _nickname.get()->c_str(); }

    bool
    Update(
        const c8 *new_nickname,
        const c8 *new_ident,
        const c8 *new_hostmask,
        const mode_update *new_modes
    );
};

IrcUser.cc

bool
IrcUser::Update(
    const c8 *new_nickname,
    const c8 *new_ident,
    const c8 *new_hostmask,
    const mode_update *new_modes
)
{
    if ( new_nickname != nullptr )
    {
        if ( _nickname == nullptr )
        {
            *_nickname = std::string(new_nickname);
        }
        else
        {
            _nickname.reset();
            *_nickname = std::string(new_nickname);
        }

        Notify(SN_NicknameChange, new_nickname);
    }

    ...
}
3个回答

8

我建议,锁定这么细粒度的级别可能会过度,因此不必要。

我建议对IrcUser对象本身进行原子更新,这取决于您的库实现和目标架构,可以是无锁的。以下是一个示例,使用:

  • std::atomic_is_lock_free<std::shared_ptr>
  • std::atomic_load<std::shared_ptr>
  • std::atomic_store<std::shared_ptr>

请参阅http://en.cppreference.com/w/cpp/memory/shared_ptr/atomic了解文档。

声明我不知道有多少编译器/C++库实现已经实现了这个C++11功能。

以下是代码示例:

#include <atomic>
#include <memory>
#include <string>

struct IrcSubject {};
typedef char c8;
typedef uint16_t u16;
typedef u16 mode_update;

class IrcUser : public IrcSubject
{
    private:
        // ...
        std::string _nickname;
        std::string _ident;
        std::string _hostmask;
        u16         _modes;
    public:
        IrcUser(std::string nickname, std::string ident, std::string hostmask, u16 modes)
            : _nickname(nickname), _ident(ident), _hostmask(hostmask), _modes(modes) { }
        // ...
        std::string const& Ident()    const { return _ident; }
        std::string const& Hostmask() const { return _hostmask; }
        const u16          Modes()    const { return _modes; }
        std::string const& Nickname() const { return _nickname; }
};

//IrcUser.cc
bool Update(std::shared_ptr<IrcUser>& user,
    std::string new_nickname,
    std::string new_ident,
    std::string new_hostmask,
    const mode_update *new_modes
)
{
    auto new_usr = std::make_shared<IrcUser>(std::move(new_nickname), std::move(new_ident), std::move(new_hostmask), *new_modes /* ??? */);
    std::atomic_store(&user, new_usr);
    //Notify(SN_NicknameChange, new_nickname);
    return true;
}

bool Foo(IrcUser const& user)
{
    // no need for locking, user is thread safe
}

int main()
{
    auto user = std::make_shared<IrcUser>("nick", "ident", "hostmask", 0x1e);

    mode_update no_clue = 0x04;
    Update(user, "Nick", "Ident", "Hostmask", &no_clue);

    {
        auto keepref = std::atomic_load(&user);
        Foo(*keepref);
    }
}

1
不得不+1,因为mode_update的猜测和位运算:P struct mode_update { bool erase_existing; u16 to_add; u16 to_remove; }; 这是个很棒的概念 - 因为IrcUser是IrcChannel中的shared_ptr,这可能会非常好用 - 正如你所说,对于这个来说太多的锁定/解锁确实是过度的。我会试一试! - ZXcvbnM

4
代码存在竞态条件,因此行为未定义,因为从不同的线程对同一对象(一个std::shared_ptr<std::string>实例)进行了潜在读(->get())和写操作(.reset()=)。访问std::shared_ptr必须进行同步。
注意,在getter内锁定std::mutex并返回c_str()是不足够的,因为getter的调用者将在锁之外使用c_str()的结果:getter需要通过值返回shared_ptr
更正方法:
  • add a std::mutex to IrcUser (note this now makes the class non-copyable):

    mutable std::mutex mtx_; // Must be mutable for use within 'const'
    
  • lock the std::mutex in the getters and Update(), using a std::lock_guard for exception safety:

    std::shared_ptr<std::string> Nickname() const
    {
        std::lock_guard<std::mutex> l(mtx_);
        return _nickname;
    }
    
    bool IrcUser::Update(const c8 *new_nickname,
                         const c8 *new_ident,
                         const c8 *new_hostmask,
                         const mode_update *new_modes)
    {
        if (new_nickname)
        {
            {
                std::lock_guard<std::mutex> l(mtx_);
                _nickname.reset(new std::string(new_nickname));
            }
            // No reason to hold the lock here.
            Notify(SN_NicknameChange, new_nickname);
        }
    
        return true;
    }
    
如果复制是可以接受的,只需使用 std::string,因为使用 shared_ptr 可能会增加不必要的复杂性。

C++11定义了std::atomic_...自由函数专门为std::shared_ptr进行特化(请参见我的答案)。我认为这里OP太快地接受了答案,这很遗憾。+1 - sehe
另一个非常好的答案 - 当我完成一些测试后,我将更改我的选择,因为这些选项提供了更多的细节和替代方案,因为需要检查一些客户端代码。 :) - ZXcvbnM
@sehe,不知道那个,谢谢(+1)。目前正在学习原子操作和内存模型。 - hmjd

1

是的,昵称可能会出现竞态条件,这会导致您的getter访问错误的内存。shared_ptr仅针对其所有权语义是线程安全的。您需要为其值添加某种形式的同步。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接