How to achieve thread synchronization---reading notes for multi-threaded server programming

Four design principles

1. Use shared objects as much as possible to reduce the need for synchronization. If an object can not be exposed to other threads, don't expose it; if you want to expose it, consider the immutable object only; if it doesn't work, you can expose the object to be modified; if it doesn't work, you can modify the exposed object and use synchronization measures to protect it.

2. Second is the use of advanced concurrent programming components, such as TaskQueue, Producer-Consumer Queue, Count DownLatch;

3. When you have to use synchronization primitives as a last resort, only use mutexes and condition variables, use read-write locks with caution, and do not use semaphores.

4. Don't write lock free code yourself, and don't use kernel-level synchronization primitives.

 

2.1 Mutex

Mutex is the most used synchronization primitive. The main purpose of using mutex alone is to use shared data. My personal principles are:

Use RAII's technique to create, destroy, lock and unlock, these four operations. Avoid forgetting

Just use non-recursive mutex.

The Lock and unlock functions are not manually called, and everything is handed over to the construction and destructor functions of the Guard object on the stack. The life cycle of Guard is exactly equal to the critical zone. In this way, we can ensure that in the same scope, automatically add and unlock.

Every time a Guard object is constructed, consider the locks that have been held along the way to prevent deadlocks caused by different locks

The secondary principles are:

Do not use non-recursive mutex

1) Locking and unlocking must be in the same thread, thread a cannot unlock the mutex that thread b has locked

2) Don't forget to unlock

3) Do not unlock repeatedly

4) Use PTHREAD_MUTEX_ERRORCHECK to troubleshoot when necessary

2.1.1 Only use non-recursive mutex

Talk about my personal thoughts about sticking to non-recursive mutex

Mutex is divided into two types: recursive and non-recursive. This is the name of posix. The other names are reentrant and non-reentrant. The difference between the two is that the same thread can repeatedly lock a reentrant lock, but cannot lock a non-recursive lock.

The preferred non-recursive mutex is definitely not for performance, but to reflect design intent. The performance gap between recursion and non-recursion is actually not big, because there is one less counter, the former is slightly faster, and
multiple use of non-recursive locks in the same thread will lead to deadlocks. This is an advantage that allows us to find shortcomings early.

There is no doubt that recursive locks are more convenient to use, and you don't need to think about locking yourself in the same thread.

It is precisely because of its convenience that recursive locks may hide some problems. You think you can modify the object if you get a lock, but the outer code has already got the lock and is modifying the same object.

Let us look at how recursive and non-recursive locks are used.

First I encapsulated a mutex

class MutexLock{
public:
    MutexLock()
    {
        pthread_mutexattr_init(&mutexattr);
        pthread_mutex_init(&mutex, nullptr);
    }

    MutexLock(int type)
    {
        int res;
        pthread_mutexattr_init(&mutexattr);
        res = pthread_mutexattr_settype(&mutexattr,type);
        pthread_mutex_init(&mutex, &mutexattr);
    }

    ~MutexLock()
    {
        pthread_mutex_destroy(&mutex);
    }

    void lock()
    {
        int res = pthread_mutex_lock(&mutex);
        std::cout<<res<<std::endl;
    }

    void unLock()
    {
        pthread_mutexattr_destroy(&mutexattr);
        pthread_mutex_unlock(&mutex);
    }
private:
    pthread_mutex_t mutex;
    pthread_mutexattr_t mutexattr;
};

Pass type in the constructor to determine the type of lock

Then we watch a demo

MutexLock mutex(PTHREAD_MUTEX_RECURSIVE);


void foo()
{
    mutex.lock();
    // do something
    mutex.unLock();
}

void* func(void* arg)
{
    mutex.lock();
    printf("3333\n");
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr,func, nullptr);
    foo();
    int res;
    mutex.lock();
    sleep(5);
    mutex.unLock();
    sleep(3);
}

In this piece of code, we found that the program did not deadlock after foo and mutex.lock in the main thread, but continued execution, which means that the recursive lock in the same thread is reentrant and does not cause deadlock.

Let's test the default lock and adjust the application method to:

MutexLock mutex(PTHREAD_MUTEX_DEFAULT);

We will find that the above program is deadlocked! !

I agree with Chen Shuo here. The advantage of using non-recursive locks is very obvious. He is very easy to find errors. Even if there is a deadlock, we can use gdb to go to the corresponding thread bt.

Of course, we can also use the attribute PTHREAD_MUTEX_ERRORCHECK_NP in c to check the error, we only need to declare the lock as follows

MutexLock mutex(PTHREAD_MUTEX_ERRORCHECK_NP);

Then we use the following program

int main()
{
    pthread_t tid;
    int res;
    res = mutex.lock();
    printf("%d\n",res);
    res = mutex.lock();
    printf("%d\n",res);
    mutex.unLock();
    printf("end\n");
}

So when we have a deadlock, we will return to EDEADLK

#define EDEADLK     35  /* Resource deadlock would occur */

Therefore, due to the perspective of easy troubleshooting for problems, I agree with the practice of using only non-recursive locks in the book, and the deadlock is not explained. The method of locating the problem is also mentioned above.

2.2 Condition variables

The mutex is to prevent contention for calculator resources and has exclusive characteristics, but if we want to wait for a certain condition to be established, then unlock

We must have learned the corresponding function in the unix network environment programming

pthread_cond_wait
pthread_cond_signal

csdn address: https://blog.csdn.net /shichao1470/article/details/89856443
Here I still have to mention a point of pthread_code_wait:
"The caller passes the locked mutex to the function, and the function automatically Put the calling thread on the list of threads waiting for the condition and unlock the mutex. This closes the
time channel between the condition check and the thread going to sleep waiting for the condition to change, so that the thread will not miss the condition Any changes. When
pthread_cond_wait returns, the mutex is locked again." pthread_cond_wait will unlock the byte!!

The amount of information in this passage is very large, and the operation of the mutex can be understood as the following three points:

1. Before calling pthread_cond_wait, you need to lock the mutex mutex before passing the &mutex into the pthread_cond_wait function
. 2. Inside the pthread_cond_wait function, the incoming mutex will be unlocked first
. 3. When the waiting condition comes, the pthread_cond_wait function is inside Will lock the incoming mutex before returning

If we need to wait for a condition to be established, we need to use condition variables. Condition variables are multiple threads or a thread waiting for a certain condition to be awakened. The scientific name of the condition variable is also called Guan Cheng!

There is only one way to use condition variables, and it is almost impossible to use them wrong. For the wait side:

1. Must be used with mutex, this boolean value must be protected by mutex

2. Wait can be called only when mutex is locked

3. Put the judgment Boolean condition and wait in the while loop

We will write an example for the above code:

We can write a simple Condition class ourselves

class Condition:noncopyable
{
public:
    //explicit用于修饰只有一个参数的构造函数,表明结构体是显示是的,不是隐式的,与他相对的另一个是implicit,意思是隐式的
    //explicit关键字只需用于类内的单参数构造函数前面。由于无参数的构造函数和多参数的构造函数总是显示调用,这种情况在构造函数前加explicit无意义。
    Condition(MutexLock& mutex) : mutex_(mutex)
    {
        pthread_cond_init(&pcond_, nullptr);
    }

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

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

    void notify()
    {
        (pthread_cond_signal(&pcond_));
    }

    void notifyAll()
    {
        (pthread_cond_broadcast(&pcond_));
    }

private:
    MutexLock& mutex_;
    pthread_cond_t pcond_;
};

Here to talk about noncopyable is mainly to prohibit copy, the core is to privatize the copy constructor

class noncopyable{
protected:
    noncopyable() = default;
    ~noncopyable() = default;

private:
    noncopyable(const noncopyable&) = delete;
    const noncopyable& operator=( const noncopyable& ) = delete;
};

The above code must use a while loop to wait for the condition variable instead of using the if statement. The reason is that spurious wakeup (false wakeup)
is also the test point of the interview.
For the signal and broadcast ends:
1. It is not necessary that the mutex has been locked. In this case, call signal (in theory).
2. Generally modify the boolean expression before signal
3. Modify the boolean expression generally to be protected by mutex
4. Pay attention to distinguish between signal and broadcast: broadcast usually indicates a state change, signal indicates that the resource is available

Here to talk about what is false awakening, if we use if to judge

if(条件满足)
{
    pthread_cond_wait();
}

pthread_cond_wait may be interrupted when signal or broadcast is not called (may be interrupted or awakened by a signal), so we must use while here

According to the example in the book, we can write a queue demo very simply

Look at the two functions of queue.cc

int dequeue()
{
    mutex.lock();
    while(queue.empty())
    {
        cond.wait();
    }
    int top = queue.front();
    queue.pop_front();
    mutex.unLock();
    return top;
}

void enqueue(int x)
{
    mutex.lock();
    queue.push_back(x);
    cond.notify();
}

Here we can think about a point together. Cond. Notify must just wake up a thread?

Cond.notify may wake up more than one thread, but if we use while to prevent false wakeup, multiple cond_wait is awakened, the kernel will essentially lock the mutex, so only
one thread will continue to execute. Do the while(queue.empty()) judgment, so it is still thread-safe

Condition variables are very low-level primitives and are rarely used directly. They are generally used for high-level synchronization measures. Just now our example said BlockingQueue. Let’s continue to learn CountDownLatch.

CountDownLatch is also a common measure of synchronization, it has two main uses:

1. The main thread initiates multiple child threads and waits for the child threads to complete certain tasks before the main thread continues to execute. Usually used for the main thread to wait for the initialization of multiple child threads to complete.

2. The main thread initiates multiple sub-threads, and the sub-threads wait for the main thread. After the main thread completes some other tasks, the sub-threads begin to execute. Usually used for multiple child threads to wait for the start command of the main thread

Let's analyze the implementation of countDownLatch in muduo, and analyze the meaning of these functions a little bit

Let's look at __attribute__ again, the code in Muze is reality

#define THREAD_ANNOTATION_ATTRIBUTE__(x)   __attribute__((x))

Let’s review __attribute__ (https://blog.csdn.net /qlexcel/article/details/92656797)

attribute__ can set function attributes, variable attributes and class attributes. The method of using __attribute__ is __attribute ((x))

__attribute__ can set the structure union, there are roughly 6 parameters can be set: aligned, packed, transparent_union, unused, deprecated, may_alias

When using __attribute__, you can also add __ underscores before and after the parameters. For example, use __aligned__ instead of aligned, so you can use it in the corresponding header file
without worrying about the header file Is there a macro definition with the same name in

1、aligned

Specify the alignment format of the object

struct S {

short b[3];

} __attribute__ ((aligned (8)));


typedef int int32_t __attribute__ ((aligned (8)));

This statement forces the compiler to ensure that the variable type is struct S or int32_t when allocating space with 8 bytes aligned. Let’s take a look at the demo results

struct S {

    short b[3];

};

The result after sizeof is 6

struct S {

    short b[3];

}__attribute__((__aligned__(8)));

After writing this way, the result is 8

2) Packed
uses this attribute to define the struct or union type and set the memory constraints of each variable of its type. Tells the compiler to cancel structure is aligned in the compilation process optimization into the actual number of occupied
lines are aligned, is gcc-specific syntax, this function has nothing to do with the operating system, with the compiler about, gcc compiler is not compact, under windwos Vc is not compact, tc programming is compact

Let's look at this example again

struct S {

    int a;
    char b;
    short c;


}__attribute__((__packed__));

This should be 8 bytes if packed is removed, but the compiler will not perform byte alignment after __packed__ is 7 bytes

3).at

Absolute positioning, variables or functions can be positioned absolutely in Flash or RAM. This software is basically not used, after all, the bottom layer of the ram board is

Well, here I want to continue to look at some very detailed usages in the muduo code

I saw a lot of interesting codes in muduo,

#ifndef MUDUO_BASE_MUTEX_H
#define MUDUO_BASE_MUTEX_H

#include "muduo/base/CurrentThread.h"
#include "muduo/base/noncopyable.h"
#include <assert.h>
#include <pthread.h>

// Thread safety annotations {
// https://clang.llvm.org/docs/ThreadSafetyAnalysis.html

// Enable thread safety attributes only with clang.
// The attributes can be safely erased when compiling with other compilers.
#if defined(__clang__) && (!defined(SWIG))
#define THREAD_ANNOTATION_ATTRIBUTE__(x)   __attribute__((x))
#else
#define THREAD_ANNOTATION_ATTRIBUTE__(x)   // no-op
#endif

#define CAPABILITY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(capability(x))

#define SCOPED_CAPABILITY \
  THREAD_ANNOTATION_ATTRIBUTE__(scoped_lockable)

#define GUARDED_BY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x))

#define PT_GUARDED_BY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(pt_guarded_by(x))

#define ACQUIRED_BEFORE(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(acquired_before(__VA_ARGS__))

#define ACQUIRED_AFTER(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(acquired_after(__VA_ARGS__))

#define REQUIRES(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(requires_capability(__VA_ARGS__))

#define REQUIRES_SHARED(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(requires_shared_capability(__VA_ARGS__))

#define ACQUIRE(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(acquire_capability(__VA_ARGS__))

#define ACQUIRE_SHARED(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(acquire_shared_capability(__VA_ARGS__))

#define RELEASE(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(release_capability(__VA_ARGS__))

#define RELEASE_SHARED(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(release_shared_capability(__VA_ARGS__))

#define TRY_ACQUIRE(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_capability(__VA_ARGS__))

#define TRY_ACQUIRE_SHARED(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_shared_capability(__VA_ARGS__))

#define EXCLUDES(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(locks_excluded(__VA_ARGS__))

#define ASSERT_CAPABILITY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(assert_capability(x))

#define ASSERT_SHARED_CAPABILITY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(assert_shared_capability(x))

#define RETURN_CAPABILITY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(lock_returned(x))

#define NO_THREAD_SAFETY_ANALYSIS \
  THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis)

The property is the first time to see, in the document there are specific explanation see URL: HTTP: //clang.llvm.org /docs/ThreadSafetyAnalysis.html

Here I mainly look at the guarded_by used in this code, let's take a look at the definition of this macro separately

#define GUARDED_BY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x))

The meaning of this macro is to lock the attribute first, and then read it. Going back to the book, I started to write code for practice. In the code, I saw another detail, which is about this mutable.

Let's take a look at the implementation of CountDownLatch

class CountDownLatch:noncopyable{
public:
    explicit CountDownLatch(int count) : mutex_(),cond_(mutex_),count_(count)
    {
    }
    void wait()
    {
        MutexLockGuard guard(mutex_);
        while(count_ > 0)
        {
            cond_.wait();
        }
    }

    void countDOwn()
    {
        MutexLockGuard guard(mutex_);
        --count_;
        if(count_ == 0)
        {
            cond_.notifyAll();
        }
    }

    int getCount() const
    {
        MutexLockGuard guard(mutex_);
        return count_;
    }

private:
    mutable MutexLock mutex_;
    Condition cond_ __attribute__((guarded_by(mutex_)));
    int count_;
};

mutable literal meaning is variable, easy to change, mutable but also to break const limitations, I have a question that is when you should use mutable, c ++ often function to operate a genus
of the members time to make changes if need be Set the attribute member to be variable.

Constant function characteristics:

1. Can only use data members can not be modified

2. Constant objects can only call constant functions, not ordinary functions

3. The this pointer of a constant function is const *

getCount is a constant function, so mutex_ must be a variable

Let's take a look at the usage of this class

CountDownLatch syncTool(10);

class CThread{
public:
    //线程进程
    static void* threadProc(void* args)
    {
        sleep(4);
        syncTool.countDOwn();
        sleep(3);
    }
};

int main()
{


    int count = 10;
    int i;
    pthread_t tid;
    pthread_t pthread_pool[count];
    CThread threadStack;
    shared_ptr<CThread> threadObj = make_shared<CThread>();
    for(i=0;i<count;i++)
    {
        pthread_create(&tid, nullptr,&CThread::threadProc, nullptr);
    }

    syncTool.wait();

    for(i=0;i<count;i++)
    {
        pthread_join(tid, nullptr);
    }

}

After the child thread wakes up, it will decrement the counter. If all child threads are initialized, then tell the main thread to continue downward

sleep is not a synchronization primitive

The sleep series of functions can only be used for testing. There are mainly two types of waiting in threads, waiting for resources to be available and waiting to enter the critical section

If in a normal program, if you need to wait for a known period of time, you should inject a timer into the event loop, and then continue to work in the timer's callback. Threads are a precious resource that cannot be easily
wasted and cannot be polled with sleep.

Realization of singleton in multithreading

The singleton in my code has been implemented like this:

T* CSingleton<T, CreationPolicy>::Instance (void)
{
    if (0 == _instance)
    {
        CnetlibCriticalSectionHelper guard(_mutex);

        if (0 == _instance)
        {
            _instance = CreationPolicy<T>::Create ();
        }
    }

    return _instance;
}

This is also a very classic realization. I have been doing this for singleton since I graduated from work.

In multithreaded server programming, use pthread_once to achieve

template <typename T>
class Singleton :noncopyable{
public:
    static T& instance()
    {
        pthread_once(&ponce_,&Singleton::init);
        return *value_;
    }

private:
    Singleton() = default;
    ~Singleton() = default;

    static void init()
    {
        value_ = new T();
    }

    static T* value_;
    static pthread_once_t ponce_;
};

It is indeed a very good note to use the kernel's api to achieve.

Guess you like

Origin blog.csdn.net/qq_32783703/article/details/105896010