Multithreaded server programming [2] - Essentials of thread synchronization

Posted by metuin on Tue, 23 Nov 2021 06:26:00 +0100

Four principles of thread synchronization

  • Minimum sharing of objects, reducing the need for synchronization
  • Use advanced concurrency components, such as TaskQueue, producer consumer queue, CountDownLatch, etc
  • When you have to use the underlying synchronization primitive, only use non recursive mutexes and conditional variables. Use read-write locks with caution and do not use semaphores
  • In addition to using atomic integers, do not write your own lock free code, and do not use kernel level synchronization primitives

Mutex (mutex)

Use principle

  • Use RAII to package the creation, destruction, locking and unlocking of mutex

    • It ensures that the effective period of the lock is equal to one scope locking, which is helpful for positioning when a deadlock occurs
    • It ensures that lock and unlock are not called manually
    • It ensures that locks are not called across processes and threads
    • It ensures that you don't forget to unlock and repeatedly lock
  • Use only non recursive mutex (i.e. non reentrant mutex)
  • Each time you construct a Guard object, think about the locks already held along the way (function call stack) to prevent deadlock caused by different order of chains

Why only use non recursive mutex

  • The same thread can repeatedly lock recursive mutex, but it cannot repeatedly lock non recursive mutex
  • Locking non recursive mutex multiple times in the same county will lead to deadlock
  • There is little difference in performance between the two

reason

  • Non recursive locks can expose programming problems early

deadlock

  • In addition to deadlocks between threads, a single thread can also cause deadlocks
  • When ensuring the use of Scoped Locking, the reason can be obtained by analyzing the call stack

Conditional variable

BlockingQueue implemented using conditional variables

muduo::MutexLock mutex;
muduo::Condition cond(mutex);
std::deque<int> queue;

int dequeue(){
    MutexLockGuard lock(mutex);
    while(queue.empty()){
        cond.wait(); // unlock mutex and wait
    }
    assert(!queue.empty());
    int top = queue.front();
    queue.pop_front();
    return top;
}

void enqueue(int x){
    {
    MutexLockGuard lock(mutex);
    queue.push_back(x);
    }
    cond.notify(); // wakeup the wait side
}
  • notify() is called every time you join instead of notifyall() to avoid the crowd shock effect
  • Not only notify() when 0 - > 1, but every time you add a notification, it can wake up multiple consumers efficiently, otherwise only one is awakened
  • See details https://book.douban.com/annot...

Countdownlatch is implemented using conditional variables

class CountDownLatch : boost::noncopyable{
    public:
        explicit CountDownLatch(int count);
        void wait();
        void countDown();
    private:
        mutable MutexLock mutex_;
        Condition condition_;
        int count_;
}

void CountDownLatch::wait(){
    MutexLockGuard lock(mutex_);
    whilt(count_>0){
        condition_.wait();
    }
}

void CountDownLatch::countDown(){
    MutexLockGuard lock(mutex_);
    --count_;
    if(count_ == 0){
        condition_.notifyAll();
    }
}

Encapsulate the implementation of MutexLock, MutexLockGuard and Condition

class MutexLock : boost::noncopyable{
    public:
        MutexLock()
        :holder_(0)
        {
            pthread_mutex_init(&mutex_NULL);
        }

        ~MutexLock()
        {
            assert(hoilder_ == 0);
            pthread_mutex_destory(&mutex);
        }

        bool isLockedByThisThread(){
            return holder_ == CurrentThread::tid();
        }
        
        void assertLocked(){
            assert(isLockedByThisThread());
        }
        
        void lock(){
            pthread_mutex_lock(&mutex);
            holder_ = CurrentThread::tid();
        }

        void unlock(){
            holder_ = 0;
            pthread_mutex_unlock();
        }

        pthread_mutex_t* getPthreadMutex(){
            return &mutex;
        }
    private:
        pthread_mutex_t mutex_;
        pid_t holder_; 
}

class MutexLockGuard : boost::noncopyable{
    public:
        explicit MutexLockGuard(MutexLock& mutex)
        :mutex_(mutex) 
        {
            mutex_.lock();
        }
        
        ~MutexLockGuard(){
            mutex_.unlock;
        }
    private:
        MutexLock& mutex_;
}

#define MutexLockGuard(x) static_assert(false,"Missing mutex guard variable name!")

class Condition : boost::noncopyable{
    public:
        explicit Condition(MutexLock& mutex)
        :mutex_(mutex)
        {
            pthread_cond_init(&pcond_,NULL);
        }

        ~Condition(){
            pthread_cond_destory(&pcond_);
        }

        void wait(){
            pthread_cond_wait(&pcond_,mutex_.getPthreadMutex());
        }

        void notify(){
            pthread_cond_signal(&pcond_);
        }
        
        void notifyAll(){
            pthread_cond_boardcast(&pcond);
        }

    private:
        MutexLock& mutex_;
        pthread_cond_t pcond_;
}

Summary

  • Four principles of thread synchronization: try to use advanced synchronization facilities
  • For other tasks, common mutexes and conditional variables are used, and RAII technique and scoped locking are used

Topics: C++ Linux Back-end Multithreading Concurrent Programming