c++ - 如何实现原子(线程安全)和异常安全的深拷贝赋值运算符?

标签 c++ assignment-operator

面试的时候被问到这个问题,我也不好回答。

更具体地说,赋值运算符所属的类如下所示:

class A {
private:
    B* pb;
    C* pc;
    ....
public:
    ....
}

如何为此类实现原子(线程安全)和异常安全的深度复制赋值运算符?

最佳答案

有两个独立的关注点(线程安全和异常安全),最好分别解决它们。为了允许构造函数将另一个对象作为参数在初始化成员时获取锁,无论如何必须将数据成员分解为一个单独的类:这样可以在初始化子对象和维护实际数据的类时获取锁可以忽略任何并发问题。因此,该类将分为两部分:class A处理并发问题和class A_unlocked来维护数据。由于A_unlocked的成员函数没有任何并发​​保护,它们不应该直接暴露在接口(interface)中,因此,A_unlocked成为 A 的私有(private)成员.

创建一个异常安全的赋值运算符是直接的,利用复制构造函数。参数被复制并交换成员:

A_unlocked& A_unlocked::operator= (A_unlocked const& other) {
    A_unlocked(other).swap(*this);
    return *this;
}

当然,这意味着一个合适的复制构造函数和一个 swap()成员被执行。处理多个资源的分配,例如,在堆上分配多个对象,最容易通过为每个对象设置合适的资源处理程序来完成。如果不使用资源处理程序,在抛出异常的情况下正确清理所有资源会很快变得非常困惑。用于维护堆分配的内存 std::unique_ptr<T> (或 std::auto_ptr<T> 如果您不能使用 C++ 2011)是一个合适的选择。下面的代码只是复制指向的对象,尽管在堆上分配对象而不是使它们成为成员没有多大意义。在一个真实的例子中,对象可能会实现一个 clone()方法或其他一些机制来创建正确类型的对象:
class A_unlocked {
private:
    std::unique_ptr<B> pb;
    std::unique_ptr<C> pc;
    // ...
public:
    A_unlocked(/*...*/);
    A_unlocked(A_unlocked const& other);
    A_unlocked& operator= (A_unlocked const& other);
    void swap(A_unlocked& other);
    // ...
};

A_unlocked::A_unlocked(A_unlocked const& other)
    : pb(new B(*other.pb))
    , pc(new C(*other.pc))
{
}
void A_unlocked::swap(A_unlocked& other) {
    using std::swap;
    swap(this->pb, other.pb);
    swap(this->pc, other.pc);
}

对于线程安全位,有必要知道没有其他线程与复制的对象混淆。这样做的方法是使用互斥锁。即,class A看起来像这样:
class A {
private:
    mutable std::mutex d_mutex;
    A_unlocked         d_data;
public:
    A(/*...*/);
    A(A const& other);
    A& operator= (A const& other);
    // ...
};

请注意,A 的所有成员如果 A 类型的对象需要做一些并发保护旨在在没有外部锁定的情况下使用。由于用于防止并发访问的互斥锁并不是对象状态的真正组成部分,但即使在读取对象状态时也需要更改,因此它是 mutable .有了这个,创建一个复制构造函数就很简单了:
A::A(A const& other)
    : d_data((std::unique_lock<std::mutex>(other.d_mutex), other.d_data)) {
}

这会锁定参数的互斥锁并委托(delegate)给成员的复制构造函数。锁在表达式结束时自动释放,与复制成功还是抛出异常无关。正在构造的对象不需要任何锁定,因为另一个线程无法知道这个对象。

赋值运算符的核心逻辑也只是委托(delegate)给基类,使用其赋值运算符。棘手的一点是有两个互斥锁需要锁定:一个用于分配给的对象,另一个用于参数。由于另一个线程可以以相反的方式分配这两个对象,因此存在死锁的可能性。方便的是,标准 C++ 库提供了 std::lock()以适当的方式获取锁以避免死锁的算法。使用此算法的一种方法是传入未锁定的 std::unique_lock<std::mutex>对象,每个需要获取的互斥量一个:
A& A::operator= (A const& other) {
    if (this != &other) {
        std::unique_lock<std::mutex> guard_this(this->d_mutex, std::defer_lock);
        std::unique_lock<std::mutex> guard_other(other.d_mutex, std::defer_lock);
        std::lock(guard_this, guard_other);

        *this->d_data = other.d_data;
    }
    return *this;
}

如果在分配期间的任何时候抛出异常,锁守卫将释放互斥锁,资源处理程序将释放任何新分配的资源。因此,上述方法实现了强异常保证。有趣的是,复制赋值需要做一个自赋值检查,以防止锁定同一个互斥锁两次。通常,我认为必要的自赋值检查表明赋值运算符不是异常安全的,但我认为上面的代码是异常安全的。

这是对答案的重大改写。此答案的早期版本要么容易丢失更新,要么容易死锁。感谢 Yakk 指出问题。虽然解决问题的结果涉及更多的代码,但我认为代码的每个单独部分实际上更简单,可以调查其正确性。

关于c++ - 如何实现原子(线程安全)和异常安全的深拷贝赋值运算符?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13030272/

相关文章:

c++ - 为什么在这种情况下需要取消引用 'this'? (赋值运算符)

c++ - std::string 内存被覆盖?

C++ WINAPI 隐藏的单选按钮没有取消选中

c++ - 数据访问器类型的常量正确性 - 更好的解决方案?

c++ - 赋值运算符的性能

c++ - Opencv矩阵范围L值: Is this a bug?

C++ const重载赋值运算符机制

c++ - 复制构造函数和赋值运算符问题

c++ - 自动模板推导中的常量正确性

c++ - 在 C/C++ 中使用常量优化的函数调用