Threads under Linux (thread synchronization and mutual exclusion)

Table of contents

Thread creation function pthread_create() under Linux

Thread waiting function pthread_join()

thread terminated

Function pthread_exit() 

Function pthread_cancel()

Detach thread pthread_detach()

synchronization between threads

amount of signal

mutual exclusion between threads

mutex 

read-write lock

deadlock


Process and thread
Thread and process are a pair of meaningful concepts. The main differences and connections are as follows:

  • A process is the basic unit for resource allocation by the operating system, and a process has a complete virtual space. When allocating system resources, in addition to CPU resources, threads are not allocated independent resources, and the resources required by threads need to be shared.
  • A thread is a part of a process. If there is no explicit thread allocation, the process can be considered single-threaded; if a thread is established in the process, the system can be considered multi-threaded.
  • Multi-threading and multi-processing are two different concepts, although both perform functions in parallel. However, resources such as memory and variables can be shared between multiple threads in a simple way. Multi-process is different, and the sharing methods between processes are limited.
  • The process has a process control table PCB, and the system schedules the process through the PCB; the thread has a thread control table TCB. However, the state represented by the TCB is much less than that of the PCB.

Threads share process data, but also own some of their own:

  • thread ID
  • a set of registers
  • the stack
  • errno
  • signal mask word
  • scheduling priority 

Multiple threads of a process share the same address space, so Text Segment and Data Segment are shared. If you define a function, it can be called in each thread. If you define a global variable, it can be accessed in each thread, except In addition, each thread shares the following process resources and environment:

  • file descriptor table
  • The processing method of each signal (SIG_ IGN, SIG_ DFL or custom signal processing function)
  • current working directory
  • user id and group id

Thread creation function pthread_create() under Linux

The function pthread_create() is used to create a thread.

When the pthread_create() function is called, the parameters passed in include thread attributes, thread functions, and thread function variables, which are used to generate a thread with certain characteristics, and the thread function is executed in the thread. Create a thread using the function pthread_create(), whose prototype is:

 #include <pthread.h>

       int pthread_create( pthread_t *thread,

                                        const pthread_attr_t *attr,
                                         void *(*start_routine) (void *),//function pointer

                                         void *arg );

  • thread: used to identify a thread, it is a variable of type pthread_ t, defined in the header file pthreadtypes.h: typedef unsigned long int pthread t;
  • attr: This parameter is used to set the attributes of the thread, set it to empty, and adopt the default attributes.
  • start_routine: When the resources of the thread are allocated successfully, the unit running in the thread is generally set to a function start_routine() written by oneself.
  • arg: The parameter passed in when the thread function is running, and a run parameter is passed in to control the end of the thread.

When the thread is created successfully, the function returns 0; if it is not 0, it means that the thread creation failed, and the common error return codes are EAGAIN and EINVAL. The error code EAGAIN indicates that the number of threads in the system has reached the upper limit, and the error code EINVAL indicates that the attribute of the thread is illegal.
After the thread is successfully created, the newly created thread determines a running function according to parameters 3 and 4, and the original thread continues to run the next line of code after the thread creation function returns. 

#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void *threadRun(void *args)
{
    while (true)
    {
        sleep(1);
        cout << "我是子线程..." << endl;
    }
}

int main()
{
    pthread_t t1;
    pthread_create(&t1, nullptr, threadRun, nullptr);

    while (true)
    {
        sleep(1);
        cout << "我是主线程..." << endl;
    }
}

At compile time, we need to link the thread library libpthread

g++ -o  myphread  myphread .cc  -lpthread 

 From the running results, we can see that the results are irregular, mainly caused by two threads competing for CPU resources.

Thread waiting function pthread_join()

Why do threads need to wait?

  • The thread that has exited, its space has not been released, and it is still in the address space of the process.
  • Creating a new thread does not reuse the address space of the thread that just exited.

The function pthread_join() is used to wait for a thread to finish running. This function is a blocking function, which waits until the end of the waiting thread before the function returns and reclaims the resources of the waiting thread. The function prototype is:

 #include <pthread.h>

       int pthread_join(pthread_t thread, void **retval);

  •  thread: The identifier of the thread, that is, the value created successfully by the pthread_create() function.
  • retval: Thread return value, which is a pointer that can be used to store the return value of the waiting thread.

The thread calling this function will hang and wait until the thread whose id is thread terminates. Thread threads are terminated in different ways, and the termination status obtained by pthread_join is different:

1. If the thread thread returns through return, the unit pointed to by retval stores the return value of the thread thread function.

2. If the thread thread is abnormally terminated by another thread calling pthread_cancel, the constant PTHREAD_CANCELED is stored in the unit pointed to by retval.

3. If the thread thread is terminated by calling pthread_exit by itself, the unit pointed to by retval stores the parameters passed to pthread_exit.

4. If you are not interested in the termination status of the thread thread, you can pass NULL to the retval parameter. 

thread terminated

If you need to terminate only a certain thread without terminating the entire process, there are three methods:

1. Return from the thread function. This method is not applicable to the main thread, and return from the main function is equivalent to calling exit.

2. A thread can terminate itself by calling pthread_exit.

3. A thread can call pthread_cancel to terminate another thread in the same process.

Function pthread_exit() 

void pthread_exit(void *retval);

 The thread termination function, like the process, has no return value, and cannot return to its caller (self) when the thread ends.

It should be noted that the memory unit pointed to by the pointer returned by pthread_exit or return must be global or allocated by malloc, and cannot be allocated on the stack of the thread function, because the thread function has already exited when other threads get the return pointer.

Function pthread_cancel()

cancel a running thread

 int pthread_cancel(pthread_t thread);

Return value: return 0 if successful; return error code if failed.

#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;
static int retvalue;//线程返回值

void *threadRun1(void *args)
{
    cout << "我是子线程1..." << endl;
    retvalue = 1;
    return (void *)&retvalue;
}
void *threadRun2(void *args)
{
    cout << "我是子线程2..." << endl;
    retvalue = 2;
    pthread_exit((void *)&retvalue);
}
void *threadRun3(void *arg)
{
    while (1)
    { 
        cout << "我是子线程3..." << endl;
        sleep(1);
    }
    return NULL;
}
int main()
{
    pthread_t tid;
    void *ret;

    // threadRun1 return
    pthread_create(&tid, NULL, threadRun1, NULL);
    pthread_join(tid, &ret);
    cout << "子线程1返回... 返回值:" << *(int *)ret << endl;

    // threadRun2 exit
    pthread_create(&tid, NULL, threadRun2, NULL);
    pthread_join(tid, &ret);
    cout << "子线程2返回... 返回值:" << *(int *)ret << endl;

    // threadRun3 cancel by other
    pthread_create(&tid, NULL, threadRun3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &ret);
    if (ret == PTHREAD_CANCELED)
        cout << "子线程3返回... 返回值:PTHREAD_CANCELED" << endl;
    else
        cout << "子线程3返回... 返回值:NULL" << endl;

}

Detach thread pthread_detach()

int pthread_detach(pthread_t thread);

The detached state of a thread determines how the thread terminates. There are two types of detached threads: detached threads and non-detached threads.
In the above example, no attribute is set when the thread is created, and the default termination method is non-detached state. In this case, it is necessary to wait for the creation thread to finish. Only when the pthread_join() function returns, the thread is considered terminated, and the resources allocated by the system when the thread was created are released.
The separation thread does not need other threads to wait. After the current thread finishes running, the thread ends and the resources are released immediately. The thread separation method can select an appropriate separation state according to the needs.
When setting a thread as a detached thread, if the thread is running very fast, it may terminate before the pthread_create() function returns. Since a thread can hand over the thread number and system resources to other threads after termination, an error will occur when using the thread number obtained by the function pthread_create() to operate.

Understanding thread libraries and thread IDs

The thread library is to provide users with an interface to create threads. The thread library will be mapped to the shared area by the process, and the threads in our process can access the code and data in the library at any time. There are related structures describing threads in the library.

 

#include<iostream>
#include<string>
#include<unistd.h>

using namespace std;

std::string hexAddr(pthread_t tid)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%x", tid);

    return buffer;
}

void *threadRoutine(void* args)
{
    string name = static_cast<const char*>(args);
    int cnt = 5;
    while(cnt)
    {
        cout << name << " : " << cnt-- << " : " << hexAddr(pthread_self()) << " &cnt: " << &cnt << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, threadRoutine, (void*)"thread 1"); // 线程被创建的时候,谁先执行不确定!
    pthread_create(&t2, nullptr, threadRoutine, (void*)"thread 2"); // 线程被创建的时候,谁先执行不确定!
    pthread_create(&t3, nullptr, threadRoutine, (void*)"thread 3"); // 线程被创建的时候,谁先执行不确定!

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);

    return 0;
}

 

 

synchronization between threads

Under the premise of ensuring data security, allowing threads to access critical resources in a specific order, thereby effectively avoiding starvation problems, is called synchronization. POSIX semaphores are used for synchronous operations to achieve conflict-free access to shared resources. POSIX can be used for inter-thread synchronization.

amount of signal

A semaphore is one of the most commonly used synchronization primitives in operating systems. A spin lock is a lock that implements busy waiting, while a semaphore allows a process to go to sleep. In simple terms, a semaphore is a counter that supports two operation primitives, P and V operations. P and V originally refer to two words in Dutch, which represent decrease and increase respectively. Later, the Americans changed it to down and up, and now these two names are also called in the Linux kernel.
The classic example of semaphores is the problem of producers and consumers. It is a classic process synchronization problem in the history of operating system development and was first proposed by Dijkstra. Assuming that producers produce commodities and consumers purchase commodities, usually consumers need to go to physical stores or online shopping malls to purchase commodities. Use a computer to simulate this scenario, one thread represents the producer, another thread represents the consumer, and the buffer represents the store. The goods produced by the producer are placed in the buffer to be supplied to the consumer thread for consumption, and the consumer thread gets the item from the buffer and then releases the buffer. If the producer thread finds that there is no free buffer available when producing goods, the producer must wait for the consumer thread to release a free buffer. If the consumer thread finds that the store is out of stock when purchasing an item, the consumer must wait until a new item is produced. If the spin lock mechanism is used, when the consumer finds that the product is out of stock, he will move a stool and sit at the door of the store and wait for the deliveryman to deliver the goods; if the semaphore mechanism is used, the store attendant will record the consumer's phone number , Wait for the arrival of the goods to notify consumers to buy. Obviously, in real life, people are willing to wait in the store if it is a product that can be prepared quickly, such as bread; if it is a product such as home appliances, people will definitely not wait in the store.

Thread semaphores are similar to process semaphores, but thread-based resource counting can be efficiently done using thread semaphores. A semaphore is actually a non-negative integer counter used to control public resources. When the public resource increases, the value of the semaphore increases; when the public resource is consumed, the value of the semaphore decreases; only when the semaphore is greater than 0, can the public resource represented by the semaphore be accessed.
The main functions of the semaphore are the semaphore initialization function sem_init(), the semaphore destruction function sem_destroy(),
The semaphore increase function sem_pom(), the semaphore decrease function sem_wait(), etc. There is also a function sem_trywait(), whose meaning is consistent with the mutually exclusive function pthread_mutex_trylock(), and first judges whether the resource is available. The prototype of the function is defined in the header file semaphore.h.
1. Thread semaphore initialization function sem_int()
The sem_int() function is used to initialize a semaphore. Its prototype is:

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

  • sem points to a pointer of the semaphore structure, when the semaphore initialization is completed, this pointer can be used to increase or decrease the semaphore;
  • The parameter pshared is used to indicate the sharing type of the semaphore. When it is not 0, the semaphore can be shared between processes, otherwise the semaphore can only be shared among multiple threads in the current process;
  • The parameter value is used to set the value of the semaphore when the semaphore is initialized.

2. Thread semaphore increase function sem_post()
sem_post (The function of the function is to increase the value of the semaphore, and the value of each increase is 1. When there is a thread waiting for this semaphore, the waiting thread will return. The prototype of the function is :

#include <semaphore.h>
int sem_post (sem_t *sem) ;

3. Thread semaphore waiting function sem_wait()
The function of the sem_wait() function is to reduce the value of the semaphore. If the value of the semaphore is 0, the thread will block until the value of the semaphore is greater than 0. The sem_wait() function reduces the value of the semaphore by 1 each time, and no longer decreases when the value of the semaphore is 0. The function prototype is:

#include <semaphore. h>
int sem_wait (sem_t *sem) ;

4. Thread semaphore destruction function sem_destroy()
The sem_destroy() function is used to release the semaphore sem. The function prototype is:

#include <semaphore.h>

int sem_destroy(sem_t *sem) ;

5. Example of a thread semaphore
Let's look at an example of using a semaphore. In the case of the mutex a global variable is used to count, in this case a semaphore is used to do the same job, where one thread increments the semaphore to mimic the producer and the other thread gets the semaphore to mimic the consumer.

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

sem_t sem;
int running = 1;//线程运行控制
int cnt = 5;

//生产者
void *producter_f (void *arg)
{
    int semval = 0;
    while (running)
    {
        usleep(1);
        sem_post(&sem);//信号量增加
        sem_getvalue(&sem,&semval);//获取信号量的值
        printf("生产,总数量:%d\n",semval); 
    }
}

//消费者
void *consumer_f (void *arg)
{
    int semval = 0;
    while (running)
    {
        usleep(1);
        sem_wait(&sem);//信号量减少
        sem_getvalue(&sem,&semval);//获取信号量的值
        printf("消费,总数量:%d\n",semval); 
    }
}
int main ()
{
    pthread_t consumer_t;//消费者线程参数
    pthread_t producter_t;//生产者线程参数
    sem_init(&sem , 0, 16);//信号量初始化
    pthread_create(&producter_t, NULL, producter_f, NULL);//建立生产者线程
    pthread_create(&consumer_t, NULL,consumer_f, NULL) ;//建立消费者线程
    usleep(1) ;//等待
    running =0;//设置线程退出值
    pthread_join(consumer_t, NULL);//等待消费者线程退出
    pthread_join (producter_t, NULL) ;//等待生产者线程退出
    sem_destroy(&sem);//信号量销毁
    return 0;

}

 It can be seen from the execution results that there is a competitive relationship among the various threads. However, the values ​​are not displayed in the order of generating one and consuming one, but in a crossed manner. Sometimes multiples are generated and then consumed. result of competition.

Semaphores have an interesting property that they can allow any number of lock holders at the same time. The semaphore initialization function is sem_init(struct semaphore *sem, int count), where the value of count can be greater than or equal to 1. When count is greater than 1, it means that there are at most count lock holders at the same time. This semaphore is called counting semaphore; when count is equal to 1, only one CPU is allowed to hold the lock at the same time. This semaphore is called a mutex semaphore or a binary semaphore. In the Linux kernel, most semaphores with a count value of 1 are used. Compared with spin locks, semaphores are a lock that allows sleep. The semaphore is suitable for some application scenarios with complex situations and relatively long locking time, such as the complex interaction between the kernel and user space.

mutual exclusion between threads

Mutex related background concept between process threads

Critical resources : resources shared by multi-threaded execution streams are called critical resources

Critical section : Inside each thread, the code that accesses critical self-entertainment is called a critical section

Mutual exclusion : At any time, mutual exclusion guarantees that one and only one execution flow enters the critical section, accesses critical resources, and usually protects critical resources

Atomicity : An operation that will not be interrupted by any scheduling mechanism. The operation has only two states, either completed or not completed

mutex

In most cases, the data used by the thread is a local variable, and the address space of the variable is in the thread stack space. In this case, the variable belongs to a single thread, and other threads cannot obtain this variable.

But sometimes, many variables need to be shared between threads. Such variables are called shared variables, and the interaction between threads can be completed through data sharing.

Multiple threads concurrently operate shared variables, which will cause some problems.

The code simulates the ticket grabbing process: 

#include <iostream>

#include <unistd.h>

#include <pthread.h>

using namespace std;

int tickets = 100;

void *threadRoutine(void *name)

{

    string tname = static_cast<const char*>(name);

    while(true)

    {

        if(tickets > 0)

        {

            usleep(2000); // Time spent simulating ticket grabbing

            cout << tname << " get a ticket: " << tickets-- << endl;

        }

        else

        {

            break;

        }

    }

    return nullptr;

}

int main()

{

    pthread_t t[4];

    int n = sizeof(t)/sizeof(t[0]);

    for(int i = 0; i < n; i++)

    {

        char *data = new char[64];

        snprintf(data, 64, "thread-%d", i+1);

        pthread_create(t+i, nullptr, threadRoutine, data);

    }

    for(int i = 0; i < n; i++)

    {

        pthread_join(t[i], nullptr);

    }

    return 0;

}

 

 

We found that due to the multi-threaded concurrent access tickets, the negative number was directly grabbed, which is unreasonable in reality. When there is only one ticket left, there are multiple threads accessing this critical resource, so we need to lock the critical resource.

mutex 

According to the size of the initial count, the semaphore can be divided into a counting semaphore and a mutual exclusion semaphore. According to the famous toilet theory, the semaphore is equivalent to a toilet that can accommodate N people at the same time. As long as the toilet is not full, others can go in. If it is full, others will wait outside. Mutual exclusion locks are similar to mobile toilets on the street. Only one person can enter at a time, and the next person in the line can only enter after the person inside comes out. Since the mutex is similar to a semaphore with a count value equal to 1, why does the kernel community redevelop the mutex instead of the mechanism of multiplexing the semaphore? At the beginning of the design, there is no problem with the implementation of the semaphore in the Linux

kernel , but mutexes are simpler and lighter than semaphores. In test scenarios with intense lock contention, mutexes are faster and more scalable than semaphores. Also, mutex data structures are less defined than semaphores. These are the advantages at the beginning of the mutex design. Some optimization schemes on mutexes (such as spin waiting) have been ported to read and write semaphores.


Thread mutual exclusion function introduction
The function prototype and initialization constants related to thread mutual exclusion are as follows, mainly including mutual exclusion initialization method macro definition, mutual exclusion initialization function pthread_mutx_init(), mutual exclusion locking function pthread_mutex_lock(), mutual exclusion The pre-lock function pthread_mutx_trylock(), the mutex unlock function pthread_mutx_unlock(), and the mutex destruction function pthread_mutex_destroy().

The function pthread_mutx_init() initializes a mutex variable, and the structure pthread_mutex_t is a private data type inside the system. When using it, it is enough to use pthread_mutex_t directly, because the system may modify its implementation. The attribute is NULL, indicating that the default attribute is used.
The pthread_mutex_lock() function declaration begins to lock with a mutex, and the code thereafter cannot execute the code in the protected area until the pthread_mutx_unlock() function is called, that is, only one thread can execute at the same time. When a thread executes to the pthread_mutex_lock() function, if the lock is used by another thread at this time, the thread will be blocked, that is, the program will wait for another thread to release the mutex. Remember to release the resource after the mutex is used, and call the pthread_mutex_destroy() function to release it.

A mutex is used to protect a critical section, which can ensure that only one thread is executing a piece of code or accessing a certain resource within a certain period of time. The following piece of code is a producer/consumer example program. The producer produces data and the consumer consumes data. They share a variable, and only one thread accesses this public variable at a time. 

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

int tickets = 100; 
class TData
{
public:
    TData(const string &name, pthread_mutex_t *mutex):_name(name), _pmutex(mutex)
    {}
    ~TData()
    {}
public:
    string _name;
    pthread_mutex_t *_pmutex;
};

void *threadRoutine(void *args)
{
    TData *td = static_cast<TData*>(args);

    while(true)
    {
        pthread_mutex_lock(td->_pmutex);
        if(tickets > 0) 
        {
            usleep(2000); // 模拟抢票花费的时间
            cout << td->_name << " get a ticket: " << tickets-- << endl;
            pthread_mutex_unlock(td->_pmutex);
        }
        else
        {
            pthread_mutex_unlock(td->_pmutex);
            break;
        }
    }

    return nullptr;
}

int main()
{

   pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);

    pthread_t tids[4];
    int n = sizeof(tids)/sizeof(tids[0]);
    for(int i = 0; i < n; i++)
    {
        char name[64];
        snprintf(name, 64, "thread-%d", i+1);
        TData *td = new TData(name, &mutex);
        pthread_create(tids+i, nullptr, threadRoutine, td);
    }

    for(int i = 0; i < n; i++)
    {
        pthread_join(tids[i], nullptr);
    }

    pthread_mutex_destroy(&mutex);
    return 0;
}

 

 

 Mutex locks are much more efficient than semaphore implementations.

  • The mutex is the first to implement the spin waiting mechanism.
  • The mutex tries to acquire the lock before sleeping.
  • Mutexes implement MCS locks to avoid CPU cache line thrashing caused by multiple CPUs competing for locks.

It is precisely because of the simplicity and efficiency of mutexes that the usage scenarios of mutexes are stricter than semaphores. The constraints that need to be paid attention to when using mutexes are as follows.

  • Only one thread can hold the mutex at a time.
  • Only the lock holder can unlock it. You cannot hold a mutex in one process and release it in another. Therefore, mutexes are not suitable for complex synchronization scenarios between the kernel and user space, and semaphores and read-write semaphores are more suitable.
  • Recursive locking and unlocking is not allowed.
  • When a process holds a mutex, the process cannot exit.
  • Mutexes must be initialized using official interface functions.
  • The mutex can sleep, so it is not allowed to be used in the interrupt handler or the lower part of the interrupt (such as tasklet, timer, etc.).
     

In actual projects, how to choose spin locks, semaphores and mutexes?
Spin locks can be used without hesitation in the interrupt context, if the critical section has sleep, implicit sleep actions and kernel interface functions , spin locks should be avoided. How to choose between semaphores and mutexes? Unless the code scenario does not meet one of the constraints of the above mutexes, mutexes can be used first.

Encapsulate threads with C++

#include <iostream>
#include <string>
#include <cstdlib>
#include <pthread.h>

class Thread
{
public:
    typedef enum
    {
        NEW = 0,
        RUNNING,
        EXITED
    } ThreadStatus;
    typedef void (*func_t)(void *);

public:
    Thread(int num, func_t func, void *args) : _tid(0), _status(NEW), _func(func), _args(args)
    {
        char name[128];
        snprintf(name, sizeof(name), "thread-%d", num);
        _name = name;
    }
    int status() { return _status; }
    std::string threadname() { return _name; }
    pthread_t threadid()
    {
        if (_status == RUNNING)
            return _tid;
        else
        {
            return 0;
        }
    }
    // 类的成员函数,具有默认参数this,需要static
    // 但是会有新的问题:static成员函数,无法直接访问类属性和其他成员函数,将this指针传过来
    static void *runHelper(void *args)
    {
        Thread *ts = (Thread*)args; // 拿到了当前对象
        // _func(_args);
        (*ts)();
        return nullptr;
    }
    void operator ()() //仿函数
    {
        if(_func != nullptr) _func(_args);
    }
    void run()
    {
        int n = pthread_create(&_tid, nullptr, runHelper, this);
        if(n != 0) exit(1);
        _status = RUNNING;
    }
    void join()
    {
        int n = pthread_join(_tid, nullptr);
        if( n != 0)
        {
            std::cerr << "main thread join thread " << _name << " error" << std::endl;
            return;
        }
        _status = EXITED;
    }
    ~Thread()
    {}

private:
    pthread_t _tid;
    std::string _name;
    func_t _func; // 线程未来要执行的回调
    void *_args;
    ThreadStatus _status;
};

Use C++ to encapsulate mutex locks

#include <iostream>
#include <pthread.h>

class Mutex // 自己不维护锁,有外部传入
{
public:
    Mutex(pthread_mutex_t *mutex):_pmutex(mutex)
    {}
    void lock()
    {
        pthread_mutex_lock(_pmutex);
    }
    void unlock()
    {
        pthread_mutex_unlock(_pmutex);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t *_pmutex;
};

class LockGuard // 自己不维护锁,由外部传入
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        _mutex.lock();
    }
    ~LockGuard()
    {
        _mutex.unlock();
    }
private:
    Mutex _mutex;
};

 use

#include <iostream>
#include <string>
#include <unistd.h>
#include "Thread.hpp"
#include "lockGuard.hpp"

using namespace std;

int tickets = 100;                                // 全局变量,共享对象
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 在外边定义的锁
void threadRoutine(void *args)
{
    std::string message = static_cast<const char *>(args);
    while (true)
    {
        {
            LockGuard lockguard(&mutex); // RAII 风格的锁
            if (tickets > 0)
            {
                usleep(2000);
                cout << message << " get a ticket: " << tickets-- << endl; // 临界区
            }
            else
            {
                break;
            }
        }
    }
}

int main()
{
    Thread t1(1, threadRoutine, (void *)"hello world");
    Thread t2(2, threadRoutine, (void *)"hello leiyaling");
    t1.run();
    t2.run();

    t1.join();
    t2.join();
    return 0;
}

Mutex locks can ensure that threads are safe, because locks can only be locked and unlocked, which are atomic. 

Thread safety : When multiple threads concurrently execute the same piece of code, different results will not appear. This problem occurs when operations on global variables or static variables are common and there is no lock protection.

Reentrancy : The same function is called by different execution flows. Before the current process is executed, other execution flows enter again. We call it reentrance. A function is called a reentrant function if there is no difference in the running result or any problems in the case of reentrancy, otherwise, it is a non-reentrant function. 

Common Thread Unsafe Situations

  • Functions that do not protect shared variables
  • A function whose state changes as it is called
  • function returning a pointer to a static variable
  • Functions that call thread-unsafe functions

Common Thread Safety Situations

  • Each thread has only read access to global variables or static variables, but no write access. Generally speaking, these threads are safe
  • Classes or interfaces are atomic operations for threads
  • Switching between multiple threads will not cause ambiguity in the execution results of this interface

Common non-reentrancy cases

  • The malloc/free function is called, because the malloc function uses a global linked list to manage the heap
  • Calls to standard I/O library functions, many implementations of the standard I/O library use global data structures in a non-reentrant manner
  • Static data structures are used in reentrant function bodies

Common reentrant cases

  • Do not use global or static variables
  • Do not use the space opened up by malloc or new
  • Non-reentrant functions are not called and do not return static or global data, all data is provided by the caller of the function
  • Use local data, or protect global data by making a local copy of global data

Reentrancy and thread safety

  • Functions are reentrant, that is, thread-safe;
  • The function is not reentrant, so it cannot be used by multiple threads, which may cause thread safety problems.
  • If a function has global variables, then the function is neither thread-safe nor reentrant.

The difference between reentrant and thread safe

  • A reentrant function is a type of thread-safe function.
  • Thread safety is not necessarily reentrant, but reentrant functions must be thread safe.
  • If the access to critical resources is locked, this function is thread-safe, but if the reentrant function has not released the lock, it will cause deadlock, so it is not reentrant.

read-write lock
 

The semaphore introduced above has an obvious shortcoming - there is no distinction between the read and write properties of the critical section. Read-write locks usually allow multiple threads to read and access the critical section concurrently, but write access is limited to only one thread. Read-write locks can effectively improve concurrency. In a multiprocessor system, multiple readers are allowed to access shared resources at the same time, but writers are exclusive. Read-write locks have the following characteristics.

  • Multiple readers are allowed to enter the critical section at the same time, but writers cannot enter at the same time.
  • Only one writer is allowed to enter the critical section at a time.
  • Readers and writers cannot enter critical sections at the same time.

 

deadlock

Deadlock refers to a permanent waiting state in which each process in a group of processes occupies resources that will not be released, but is in a permanent waiting state due to mutual application for resources that are used by other processes and will not be released.

Four necessary conditions for deadlock

  • Mutually exclusive conditions: a resource can only be used by one execution flow at a time
  • Request and Hold Conditions: When an execution flow is blocked by requesting resources, hold on to the obtained resources
  • Non-deprivation condition: The resource obtained by an execution flow cannot be forcibly deprived before it is used up
  • Circular waiting condition: A head-to-tail cyclic waiting resource relationship is formed between several execution flows

avoid deadlock

  • Four necessary conditions to break deadlock
  • The order of locking is the same
  • Avoid scenarios where locks are not released
  • One-time allocation of resources 

Guess you like

Origin blog.csdn.net/m0_55752775/article/details/130688298