Linux (muduo network libraries): 01 --- lifetime management of the object thread-safe (constructor and destructor of objects in multiple threads)

  • Write thread-safe class is not difficult, the state can use to protect the internal synchronization primitives. But life and death can not have objects with mutex (mutual exclusion device) by the object itself to protect
  • How to avoid possible object destruction race condition (race conditions) is a C ++ fundamental problem facing the multithreaded programming, can use Boost library shared_ptr and weak_ptr perfect solution. This is also the realization of thread-safe mode Observer essential technology

First, when destructor encountered multithreaded

  • Different from other object-oriented languages, C ++ requires the programmer to manage the lifetime of the object, which is especially difficult in a multithreaded environment . When an object that can be seen multiple threads simultaneously, so the timing of the destruction of the object becomes blurred, various race conditions may occur :

    • In the forthcoming destruction of an object, where with knowledge at the moment whether there is another thread is executing a member function of the object?
    • How to ensure that during the execution of a member function, the object will not be destructed in another thread?
    • Before calling the member function of an object , the object is still alive How do I know this? It's destructor will not happen to perform half?
  • To solve these basic problems are race condition C ++ multi-threaded programming facing. This article and the next few articles shared_ptr trying to solve these problems once and for all, relieve the mental burden C ++ multi-threaded programming

Thread-safe definition

  • According to the [JCP], a thread-safe class should meet the following three conditions:
    • When multiple threads access, which exhibits the correct behavior
    • No matter how the operating system scheduling these threads, regardless of the order in which these threads are interwoven (interleaving)
    • Call-side code without additional synchronization or other coordination actions
  • According to this definition, most of the class C ++ standard library is not thread-safe , including std :: string, std :: vector, std :: map , as these usually need to lock in an external class to supply multiple threads simultaneously access

MutexLock and MutexLockGuard class

  • To facilitate discussion hereinafter, to two tools agreement (see §2.4 Code)
    • MutexLock: Packaging critical region (critical section), this is a simple resource with an exclusive package to create RAII techniques and destroyed. MutexLock general data members of another class
      • The critical zone on Windows struct CRITICAL_SECTION, is reentrant
      • Under Linux is pthread_mutex_t, default is not reentrant
    • MutexLockGuard: enter and exit the critical section of the package, namely locking and unlocking. MutexLockGuard general is on the stack object, its scope is exactly equal to the critical region
  •  Both class are not allowed to copy constructor and assignment, their use doctrine, see §2.1

Single-threaded safe Counter example

  • Writing a single thread-safe class is not too difficult, just use synchronization primitives to protect its internal state
  • The following simple example class counter Counter:
class Counter :boost::noncopyable
{
public:
    Counter() :value_(0) {}
    int64_t value()const;
    int64_t getAndIncrease();
private:
    int64_t value_;
    mutable MutexLock mutex_;
};

int64_t Counter::value()const
{
    MutexLockGuard lock(mutex_); //lock的析构会晚于返回对象的构造,因此有效地保护了这个共享数据
    return value_;
}

int64_t Counter::getAndIncrease()
{
    MutexLockGuard lock(mutex_);
    int64_t ret = value_++;
    return ret;
}

//实际项目中,这个class用原子操作更合理,这里用锁仅仅为了举例
  • This class is very straightforward, one can understand, it is easy to verify that it is thread-safe. Each object has its own Counter mutex_, and therefore does not constitute a lock contention between different objects (lock contention). That is possible to simultaneously execute two threads getAndIncrease () function in the value _ ++, provided that they are not accessible with a Counter Object
  • Notes that its mutex_ members are mutable, which means const member functions such as Counter :: value () can also be used as non-const of mutex_ . Question: If mutex_ static, it affect the validity and / or performance?
  • Although this in itself is no doubt Counter is thread-safe, but if Counter is dynamically created and accessed through a pointer, the object mentioned earlier destroyed race condition still exists

Second, create a very simple object

  • To do a thread-safe object construction, the only requirement is not to divulge this pointers during construction, namely:
    • Do not register any callback in the constructor
    • Do not put this pass objects across threads in the constructor
    • Even in the last line of the constructor does not work
  • The reason for this provision is not yet completed because the object is initialized during the constructor execution , if this is compromised (escape) to the other objects (except its own sub-objects created), then other threads may have access to this semi-finished objects, this will cause unpredictable consequences
  • The following constructor is not thread-safe:
class Foo :public Observer //Observer的定义见下文
{
public:
    Foo(Observable* s)
    {
        s->register_(this); //此处非线程安全
    }

    virtual void update();
};
  • The correct way is to construct an object:
class Foo :public Observer
{
public:
    Foo();
    virtual void update();

    //另外定义一个函数,在构造之后执行回调函数的注册工作
    void observer(Observable* s)
    {
        s->register_(this);
    }
};
  • This also shows that the two-stage structure - namely Constructor + initialize () - is sometimes a good idea , though it does not meet the C ++ dogma, but a multithreaded no choice. In addition, since the two-stage configuration allows, then the constructor does not have to take the initiative to throw an exception, the caller by initialize () return value to determine whether the object structure success, which can simplify error handling
  • Even if the last line of the constructor and do not leak this:
    • Because there may be Foo base class, the base class to the derived class constructor before, after executing the last line of code will Foo :: Foo () of the derived class constructor to continue, then the most-derived class object is in configuration still unsafe
  • Relatively speaking, the constructed object thread-safe is relatively easy to do, after all, less exposure, keep them coming back to zero. The thread-safe destruction is not so simple, and this is the focus of this article and the following articles of interest

Third, the destruction is too difficult

  • Object destruction, which is not an issue in a single thread , take up to avoid dangling pointers and pointer field
  • And in a multithreaded program, there are too many race conditions . For most member functions do thread-safe way is to let them execute sequentially rather than concurrently (the key is not simultaneously read and write shared state), that is, to make the critical area of each member functions do not overlap. This is obvious, but there is an implied condition may not be everyone immediately thought: member function is critical to protect the exclusive zone itself must be valid. The destructor destroy this hypothesis, it will mutex member variables in destroying

mutex is not the answer

  • mutex can only guarantee one by one to perform a function
  • Consider the following code:
    • I tried to protect the mutex destructor
    • There is also a function to use mutex

  • In this case, assuming that there are A, B two threads can see the Foo object x:
    • A thread about to destroy x
    • And thread B is preparing to call x-> update ()

  • Although the thread after the destruction of the object A pointer is set to NULL, even though thread B to check the value of x pointer before calling the member function of x, but still can not avoid one kind of race condition:
    • A thread to execute destructor (1), already owns the mutex, would continue to remain down
    • Through the thread B if (x) detection, blocking at (2)
  • What happens next, only God knows. Because the destructor will mutex_ destroyed, (2) at the blockage may never go down , it is possible to enter the "critical area", then core dump, or other worse happens
  •  After this example shows the object delete at least a pointer to NULL useless, if a program to prevent secondary rely on the release, a problem described code logic

As a mutex protected data members can destructor

  • Examples of the foregoing description, as MutexLock class data members only for reading and writing data to other members of this class of sync, it can not be safely protected destructor . because:
    • Like most long-lived MutexLock members of the object, and the destructor action can be said to occur after the object death (or the time of death)
    • Further, for the base class object, then calling the destructor of the base class, when the portion of the derived class objects have been destructed, then the base class object has MutexLock not protect the entire destruction process
    • Besides, destruction process would have been no need to be protected, because only less than other threads access the object, the destructor is safe , otherwise there will be a race condition occurs talked about the beginning of the article
  • Also if you want to read and write at the same time two objects of a class, you may have potential deadlock . for example:
    • There swap () function
    • If thread A executed swap (a, b); while the thread B executed swap (b, a) ;, possible deadlocks
void swap(Counter& a, Counter& b)
{
    //potential dead lock(潜在的死锁)
    MutexLockGuard aLock(a.mutex_);
    MutexLockGuard bLock(b.mutex_);

    //swap
    int64_t value = a.value_;
    a.value_ = b.value_;
    b.value_ = value;
}
  • operator = () is similar reasons, it may produce a deadlock . E.g:
Counter& Counter::operator=(const Counter& rhs)
{
    if (&rhs == this)
        return *this;

    //potential dead lock(潜在的死锁)
    MutexLockGuard myLock(mutex_);
    MutexLockGuard itsLock(rhs.mutex_);
    value_ = rhs.value_; //如果该位value_=rhs.value()就会产生死锁,因为value()函数也请求锁住互斥量
    return *this; 
}
  • If you want to lock a function of multiple objects of the same type, in order to ensure the lock is always in the same order, we can compare the mutex object's address, Always lock mutex smaller address

IV Summary

Released 1504 original articles · won praise 1063 · Views 430,000 +

Guess you like

Origin blog.csdn.net/qq_41453285/article/details/104720277