简体   繁体   English

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

[英]How to implement an atomic (thread-safe) and exception-safe deep copy assignment operator?

I was asked this question in an interview, and I couldn't answer it well. 我在接受采访时被问到这个问题,我无法回答这个问题。

More specifically, the class to which the assignment operator belongs looks like this: 更具体地说,赋值运算符所属的类如下所示:

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

How to implement an atomic (thread-safe) and exception-safe , deep copy assignment operator for this class? 如何为此类实现原子 (线程安全)和异常安全的深层复制赋值运算符?

There are two separate concerns (thread-safety and exception-safety) and it seems best to address them separately. 有两个独立的问题(线程安全和异常安全),似乎最好单独解决它们。 To allow constructors taking another object as argument to acquire a lock while initializing the members, it is necessary to factor the data members into a separate class anyway: This way a lock can be acquired while the subobject is initialized and the class maintaining the actual data can ignore any concurrency issues. 为了允许构造函数在初始化成员时将另一个对象作为参数获取锁,有必要将数据成员分解为一个单独的类:这样就可以在初始化子对象时获取锁,并保持实际数据的类可以忽略任何并发问题。 Thus, the class will be split into two parts: class A to deal with concurrency issues and class A_unlocked to maintain the data. 因此,该类将分为两部分: class A处理并发问题, class A_unlocked维护数据。 Since the member functions of A_unlocked don't have any concurrency protection, they shouldn't be directly exposed in the interface and, thus, A_unlocked is made a private member of A . 由于A_unlocked的成员函数没有任何并发​​保护,因此它们不应直接暴露在接口中,因此, A_unlocked成为A的私有成员。

Creating an exception-safe assignment operator is straight forward, leveraging the copy constructor. 创建异常安全的赋值运算符是直截了当的,利用了复制构造函数。 The argument is copied and the members are swapped: 复制参数并交换成员:

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

Of course, this means that a suitable copy constructor and a swap() member are implemented. 当然,这意味着实现了合适的拷贝构造函数和swap()成员。 Dealing with the allocation of multiple resources, eg, multiple objects allocated on the heap, is easiest done by having a suitable resource handler for each of the objects. 通过为每个对象提供合适的资源处理程序,最简单地处理多个资源的分配,例如,在堆上分配的多个对象。 Without the use of resource handlers it becomes quickly very messy to correctly clean up all resources in case an exception is thrown. 如果不使用资源处理程序,在抛出异常时正确清理所有资源会很快变得非常混乱。 For the purpose of maintaining heap allocated memory std::unique_ptr<T> (or std::auto_ptr<T> if you can't use C++ 2011) is a suitable choice. 为了维护堆分配的内存std::unique_ptr<T> (或std::auto_ptr<T>如果你不能使用C ++ 2011)是一个合适的选择。 The code below just copies the pointed to objects although there isn't much point in allocating the objects on the heap rather than making them members. 下面的代码只是复制指向的对象,尽管在堆上分配对象而不是将它们作为成员没有多大意义。 In a real example the objects would probably implement a clone() method or some other mechanism to create an object of the correct type: 在一个真实的例子中,对象可能会实现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);
}

For the thread-safety bit it is necessary to know that no other thread is messing with the copied object. 对于线程安全位,有必要知道没有其他线程正在弄乱复制的对象。 The way to do this is using a mutex. 这样做的方法是使用互斥锁。 That is, class A looks something like this: 也就是说, 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);
    // ...
};

Note, that all members of A will need to do some concurrency protection if the objects of type A are meant to be used without external locking. 注意,如果要在没有外部锁定的情况下使用类型A的对象,则A所有成员都需要执行一些并发保护。 Since the mutex used to guard against concurrent access isn't really part of the object's state but needs to be changed even when reading the object's state, it is made mutable . 由于用于防止并发访问的互斥锁实际上不是对象状态的一部分,但即使在读取对象的状态时也需要更改它,因此它是mutable With this in place, creating a copy constructor is straight forward: 有了这个,创建一个复制构造函数是直截了当的:

A::A(A const& other)
    : d_data((std::unique_lock<std::mutex>(other.d_mutex), other.d_data)) {
}

This locks the argument's mutex and delegates to the member's copy constructor. 这会将参数的互斥锁和委托锁定到成员的复制构造函数。 The lock is automatically released at the end of the expression, independent of whether the copy was successful or threw an exception. 无论复制是成功还是抛出异常,锁都会在表达式结束时自动释放。 The object being constructed doesn't need any locking because there is no way that another thread knows about this object, yet. 正在构造的对象不需要任何锁定,因为其他线程无法知道该对象。

The core logic of the assignment operator also just delegates to the base, using its assignment operator. 赋值运算符的核心逻辑也只是使用赋值运算符委托给基数。 The tricky bit is that there are two mutexes which need to be locked: the one for the object being assigned to and the one for the argument. 棘手的一点是有两个需要锁定的互斥锁:一个用于被分配的对象,另一个用于参数。 Since another thread could assign the two objects in just the opposite way, there is a potential for dead-lock. 由于另一个线程可以以相反的方式分配这两个对象,因此存在死锁的可能性。 Conveniently, the standard C++ library provides the std::lock() algorithm which acquires locks in an appropriate way that avoids dead-locks. 方便的是,标准C ++库提供了std::lock()算法,该算法以适当的方式获取锁,以避免std::lock() One way to use this algorithm is to pass in unlocked std::unique_lock<std::mutex> objects, one for each mutex needed to be acquired: 使用此算法的一种方法是传入未锁定的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;
}

If at any point during the assignment an exception is thrown, the lock guards will release the mutexes and the resource handlers will release any newly allocated resource. 如果在分配期间的任何时刻抛出异常,则锁定保护将释放互斥锁,资源处理程序将释放任何新分配的资源。 Thus, the above approach implements the strong exception guarantee. 因此,上述方法实现了强大的异常保证。 Interestingly, the copy assignment needs to do a self-assignment check to prevent locking the same mutex twice. 有趣的是,复制分配需要进行自我分配检查以防止锁定相同的互斥锁两次。 Normally, I maintain that a necessary self-assignment check is an indication that the assignment operator isn't exception safe but I think the code above is exception safe. 通常,我认为必要的自我赋值检查表明赋值运算符不是异常安全的,但我认为上面的代码是异常安全的。

This is a major rewrite of the answer. 这是对答案的重大改写。 Earlier versions of this answer were either prone to a lost update or to a dead-lock. 此答案的早期版本要么容易丢失更新,要么发生死锁。 Thanks to Yakk for pointing out the problems. 感谢Yakk指出了问题所在。 Although the result of addressing the issues involves more code, I think each individual part of the code is actually simpler and can be investigated for correctness. 虽然解决问题的结果涉及更多代码,但我认为代码的每个单独部分实际上都更简单并且可以进行调查以确保正确性。

First, you must understand that no operation is thread safe, but rather all operations on a given resource can be mutually thread safe. 首先,您必须了解任何操作都不是线程安全的,而是对给定资源的所有操作都可以是相互线程安全的。 So we must discuss the behavior of non-assignment operator code. 所以我们必须讨论非赋值运算符代码的行为。

The simplest solution would be to make the data immutable, write an Aref class that uses the pImpl class to store an immutable reference counted A, and have mutating methods on Aref cause a new A to be created. 最简单的解决方案是使数据不可变,编写一个使用pImpl类来存储不可变引用计数A的Aref类,并在Aref上使用变异方法导致创建新的A. You can achieve granularity by having immutable reference counted components of A (like B and C) follow a similar pattern. 您可以通过使A的不可变引用计数组件(如B和C)遵循类似的模式来实现粒度。 Basically, Aref becomes a COW (copy on write) pImpl wrapper for an A (you can include optimizations to handle single-reference cases to do away with the redundant copy). 基本上,Aref成为A的COW(写入时复制)pImpl包装器(您可以包括优化以处理单引用情况以消除冗余副本)。

A second route would be to create a monolithic lock (mutex or reader-writer) on A and all of its data. 第二种方法是在A及其所有数据上创建单片锁(互斥或读写器)。 In that case, you either need mutex ordering on the locks for instances of A (or a similar technique) to create a race-free operator=, or accept the possibly surprising race condition possibility and do the copy-swap idiom Dietmar mentioned. 在这种情况下,您需要对A(或类似技术)的实例锁定进行互斥排序,以创建无竞争的运算符=,或接受可能令人惊讶的竞争条件的可能性,并执行Dietmar提到的复制交换习惯。 (Copy-move is also acceptable) (Explicit race condition in the lock-copyconstruct, lock-swap assignment operator=: Thread1 does X=Y. Thread 2 does Y.flag = true, X.flag = true. State afterwards: X.flag is false. Even if Thread2 locks both X and Y over the entire assignment, this can happen. This would surprise many programmers.) (复制 - 移动也是可以接受的)(lock-copyconstruct中的显式竞争条件,锁定交换赋值运算符=:Thread1执行X = Y.线程2执行Y.flag = true,X.flag = true。后续状态:X .flag是假的。即使Thread2在整个赋值中锁定X和Y,也会发生这种情况。这会让很多程序员感到惊讶。)

In the first case, non-assignment code has to obey the copy-on-write semantics. 在第一种情况下,非赋值代码必须遵守写时复制语义。 In the second case, non-assignment code has to obey the monolithic lock. 在第二种情况下,非赋值代码必须服从单片锁。

As for exception safety, if you presume your copy constructor is exception safe, as is your lock code, the lock-copy-lock-swap one (the second) is exception safe. 至于异常安全性,如果你认为你的拷贝构造函数是异常安全的,那么你的锁代码就是这样,lock-copy-lock-swap one(第二个)是异常安全的。 For the first one, so long as your reference counting, lock clone and data modification code is exception safe you are good: the operator= code is pretty brain dead in either case. 对于第一个,只要您的引用计数,锁定克隆和数据修改代码是异常安全的,您就是好的:在任何一种情况下,operator =代码都非常大脑死亡。 (Make sure your locks are RAII, store all allocated memory in a std RAII pointer holder (with the ability to release if you end up handing it off), etc.) (确保你的锁是RAII,将所有已分配的内存存储在std RAII指针支架中(如果最终将其关闭,则可以释放),等等)

Exception-safe? 异常安全? Operations on primitives don't throw so we can get that for free. 基元的操作不会抛出,所以我们可以免费获得。

Atomic? 原子? The simplest would be an atomic swap for 2x sizeof(void*) - I believe that most platforms do offer this. 最简单的是2x sizeof(void*)的原子交换 - 我相信大多数平台都提供此功能。 If they don't, you'd have to resort to either using a lock, or there are lockless algorithms which can work. 如果他们不这样做,你必须使用锁,或者有无锁算法可以工作。

Edit: Deep copy, huh? 编辑:深拷贝,对吧? You'd have to copy A and B into new temporary smart pointers, then atomically swap them. 你必须将A和B复制到新的临时智能指针中,然后以原子方式交换它们。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM