Linux multithreaded server programming notes-1

Keywords: C++ Back-end Multithreading

Thread safe object creation, callback and Deconstruction in C + +

  • It is not difficult to write thread safe classes. You can use synchronization primitives to protect the internal state
  • Most STL classes are not thread safe and need to be locked externally to ensure simultaneous access by multiple threads

Secure object creation

The only requirement is not to disclose this pointer during construction, that is

- Do not register any callbacks in the constructor
- Don't put this Pointer to cross thread object
- Not even on the last line, because this class may be a base class, and the last line of its constructor does not equal the completion of construction

Reason: the object did not complete initialization during the execution of the constructor. If the this pointer is written and leaked to other objects, other threads may access the semi-finished product

Secure object destruction

Race condition of object destructor

1.When analyzing an object, how do you know if someone else is using it?
2.When destructing an object, is it possible that it is being destructed by another thread?
3.When calling a member function of an object, how to ensure that it is still alive? Does the destructor happen to be halfway through?

Difficulties in designing object destructors

1.mutex is not the solution, because the lock is only used to protect the internal data of the object, not the object itself, and can not solve any problems

2. The three object relationships widely used in object-oriented depend on the ability of objects to know whether other objects are still alive

a. composition
b. aggregation
c. association

Among them, composition is not troublesome in multithreading, because the life cycle of object x is uniquely controlled by its owner, but the connection between the latter two objects is realized by pointers or references, so the race conditions mentioned above may occur.

Solution: shared_ptr / weak_ptr

That is, an intermediate layer is introduced between objects. Instead of using bare pointers, smart pointers are used as an auxiliary Manager (reference counting).

  • shared_ptr controls the life cycle of an object. It is a strong reference, as long as there is a shared object pointing to an X object_ If PTR exists, x will not destruct. When the reference count drops to 0 or reset(), X is guaranteed to be destroyed.
  • weak_ptr does not control the object life cycle, but it knows whether the object is or not. If the object is not dead, it can be promoted to a valid shared_ptr, so as to reference the object and ensure that the object does not destruct during this period. If the object dies, an empty shared will be returned_ ptr. Promotion behavior is thread safe.
  • shared_ptr and weak_ptr counting is an atomic operation with good performance

however

  • ❗ shared_ptr and weak_ The thread safety level of PTR itself is the same as that of std::string and STL containers. See the following discussion

About shared_ Discussion on PTR

  • shared_ptr is not thread safe: it can be read by multiple threads at the same time, and cannot be written by multiple threads at the same time (destructor write)
  • shared_ The thread safety of PTR is different from that of the object it points to
  • Access shared_ptr, need to add mutex

    void read(){
      shared_ptr<Foo> localPtr;
      {
          MutexLockGuard lock(mutex);
          localPtr = globalPtr; // read globalPtr
      }
      // use localPtr since here, there is no need to lock reading and writing localPtr, because it is an object on the stack, which is visible only to the current thread
      do_it(localPtr); // Here, the parameter of the function should be reference to const to avoid duplication
    }
  • weak_ptr and shared_ If PTR is used incorrectly, some objects may never be destructed. The common practice is that the owner has a shared pointing to the child_ PTR, child holds owner's weak_ptr
  • Copy cost: because a copy will be created, the reference count needs to be modified. The cost is large, but there are few places to copy, usually shared_ptr as a parameter is const reference.

RAII resource acquisition is initialized

  • The most important feature that distinguishes C + + from other programming languages
  • Each explicit resource configuration action (such as new) should be executed in a single statement, and the resources obtained from the configuration should be immediately handed over to the handle object (such as shared_ptr) in the sojourn. Generally, delete does not appear in the program

Thread safe object pool

Implement a StcokFactory and return the Stock object according to the key

Version 1: using weak_ptr and shared_ptr management

class StockFactory : boost::noncopyable
{
 public:
  boost::shared_ptr<Stock> get(const string& key)
  {
    boost::shared_ptr<Stock> pStock;
    muduo::MutexLockGuard lock(mutex_);
    boost::weak_ptr<Stock>& wkStock = stocks_[key]; // If this is shared_ptr, the object will never be destructed
    pStock = wkStock.lock();
    if (!pStock)
    {
      pStock.reset(new Stock(key));
      wkStock = pStock; // wkStock is a reference 
    }
    return pStock;
  }

 private:
  mutable muduo::MutexLock mutex_;
  std::map<string, boost::weak_ptr<Stock> > stocks_;
};

Problem: there is a slight memory leak. The key: weak in the map_ PTR will always exist. There is no problem when the key is limited. Memory leakage will occur when the key range is large

Version 2: using shared_ptr's custom destruct function destructs objects at the same time

class StockFactory : boost::noncopyable
{
 public:

  boost::shared_ptr<Stock> get(const string& key)
  {
    boost::shared_ptr<Stock> pStock;
    muduo::MutexLockGuard lock(mutex_);
    boost::weak_ptr<Stock>& wkStock = stocks_[key];
    pStock = wkStock.lock();
    if (!pStock)
    {
      pStock.reset(new Stock(key),
                   boost::bind(&StockFactory::deleteStock, this, _1)); // The custom destructor function binds the member function of an object
      wkStock = pStock;
    }
    return pStock;
  }

 private:

  void deleteStock(Stock* stock)
  {
    printf("deleteStock[%p]\n", stock);
    if (stock)
    {
      muduo::MutexLockGuard lock(mutex_);
      stocks_.erase(stock->key());  // If stocks_ Die before stock, here will be core dump
    }
    delete stock;  // Because you customize the destructor function, you need to write delete manually
  }
  mutable muduo::MutexLock mutex_;
  std::map<string, boost::weak_ptr<Stock> > stocks_;
};

Question: if stocks_ Die before stock, destruct stock, callback stocks_ The essence of this problem is the same as that discussed above, that is, the this pointer of the object is not enough to judge the object life cycle. In the above method, use weak instead_ ptr + shared_ PTR solves the problem, and so does here

Version 3: using enable_shared_from_this uses shared_ptr instead of this pointer

class StockFactory : public boost::enable_shared_from_this<StockFactory>,
                     boost::noncopyable
{
 public:

  boost::shared_ptr<Stock> get(const string& key)
  {
    boost::shared_ptr<Stock> pStock;
    muduo::MutexLockGuard lock(mutex_);
    boost::weak_ptr<Stock>& wkStock = stocks_[key];
    pStock = wkStock.lock();
    if (!pStock)
    {
      pStock.reset(new Stock(key),
                   boost::bind(&StockFactory::deleteStock,
                               shared_from_this(),
                               _1));
      wkStock = pStock;
    }
    return pStock;
  }

 private:

  void deleteStock(Stock* stock)
  {
    printf("deleteStock[%p]\n", stock);
    if (stock)
    {
      muduo::MutexLockGuard lock(mutex_);
      stocks_.erase(stock->key());  // This is wrong, see removeStock below for correct implementation.
    }
    delete stock; 
  }
  mutable muduo::MutexLock mutex_;
  std::map<string, boost::weak_ptr<Stock> > stocks_;
};

Problem: because stock registers the shared of StockFactory_ PTR (pointer copying occurs during bind), and the lead StockFactory will not destruct until all stocks are destructed, which unexpectedly prolongs its life.

Version 4: using enable_shared_from_this uses weak_ptr replaces this pointer to implement weak callback: "if the object is still alive, call its member function, otherwise ignore it"

class StockFactory : public boost::enable_shared_from_this<StockFactory>,
                     boost::noncopyable
{
 public:
  boost::shared_ptr<Stock> get(const string& key)
  {
    boost::shared_ptr<Stock> pStock;
    muduo::MutexLockGuard lock(mutex_);
    boost::weak_ptr<Stock>& wkStock = stocks_[key];
    pStock = wkStock.lock();
    if (!pStock)
    {
      pStock.reset(new Stock(key),
                   boost::bind(&StockFactory::weakDeleteCallback,
                               boost::weak_ptr<StockFactory>(shared_from_this()),
                                // This transformation is necessary to avoid prolonging the life cycle of StockFactory
                                // boost:bind copies the argument type, not the formal parameter type
                               _1));
      wkStock = pStock;
    }
    return pStock;
  }

 private:
  static void weakDeleteCallback(const boost::weak_ptr<StockFactory>& wkFactory,
                                 Stock* stock)
  {
    printf("weakDeleteStock[%p]\n", stock);
    boost::shared_ptr<StockFactory> factory(wkFactory.lock());
    if (factory)
    {
      factory->removeStock(stock);
    }
    else
    {
      printf("factory died.\n");
    }
    delete stock; 
  }

  void removeStock(Stock* stock)
  {
    if (stock)
    {
      muduo::MutexLockGuard lock(mutex_);
      auto it = stocks_.find(stock->key());
      if (it != stocks_.end() && it->second.expired())
      {
        stocks_.erase(stock->key());
      }
    }
  }

 private:
  mutable muduo::MutexLock mutex_;
  std::map<string, boost::weak_ptr<Stock> > stocks_;
};

In this case, whoever first destructs Stock or StockFactory will not affect the normal operation of the program. The problem of mutual reference between two objects is solved by using intelligent pointers.
Of course, the Factory object is usually a singleton and will not be destroyed during normal operation. This is just to show the weak callback count.

Tips for multithreaded programming

1. The synchronization primitive at the bottom of the package locks and unlocks with the help of object construction and deconstruction

int getvalue() const{
    MutexLockGuard lock(mutex_); // Object construction is locking
    return value_; // Read data
} // When the scope ends, Guard automatically destructs and unlocks

2. If a function wants to lock two objects of the same type, in order to ensure that they are always locked in the same order, you can compare the mutex object whose address is always locked with the smaller address, such as:

void swap(counter& a, counter& b){
    MutexLockGuard aLock(a.mutex_);
    MutexLockGuard bLock(b.mutex_);
    // Exchange data of a and b
}

If thread A executes swap(a,b) and thread B executes swap(b,a), A deadlock will occur.

3. Although this chapter talks about how to safely use and deconstruct cross thread objects, it is a better multi-threaded programming method to try not to use cross thread objects and use regular mechanisms such as pipeline, producer, consumer and task queue to share data at a minimum

Posted by Rottingham on Mon, 08 Nov 2021 01:24:48 -0800