Multithreading - mutual exclusion and synchronization

Multithreading—mutual exclusion and synchronization

Bitmap(8)

Multi-thread mutual exclusion

Several concepts should be introduced here

  • Critical resources: Resources shared by multi-threaded execution flows are called critical resources.

The ticket in the example is the global data of the main thread and is a critical resource here.

  • Critical section: Inside each thread, the code that accesses critical resources is called the critical section.

Ticket is a critical resource, so subsequent operations such as judging ticket>0 and ticket– are codes to access critical resources and belong to the critical section.

  • Atomicity (how to implement it will be discussed later): An operation that will not be interrupted by any scheduling mechanism. This operation has only two states, either completed or incomplete.

The code in the for loop, that is, the critical section will be thread-switched halfway through execution, so the critical section is not atomic, and it is for this reason that the bug is caused

  • Parallelism: refers to two or more events occurring at the same time, multiple events on different entities. Generally suitable for multi-CPU situations

Multi-core CPU truly realizes "executing multiple tasks at the same time". Each core of the multi-core CPU can independently perform a task, and the multiple cores will not interfere with each other. Multiple tasks executed on different cores are truly running at the same time. This state is called parallelism.

The figure below shows the process of parallel execution of two tasks:

image-20230714164204460

  • Concurrency refers to two or more events occurring at the same time interval, which are multiple events on the same entity.

It uses an algorithm to reasonably allocate CPU resources to multiple tasks. When a task performs an I/O operation, the CPU can switch to other tasks until the I/O operation is completed, or a new task encounters an I/O operation. During the /O operation, the CPU returns to the original task and continues execution.

The following figure shows the process of concurrent execution of two tasks:

image-20230714164516485

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

Mutual exclusion enables multiple threads to access critical resources serially, protects critical resources, and effectively avoids multi-threaded concurrency errors.

mutex mutex

  • In most cases, the data used by threads are local variables, and the address space of the variable is within 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 operating shared variables concurrently will cause some problems.

Here we only analyze single CPU

First simulate a multi-threaded program to grab train tickets. When each thread enters the loop, it first determines whether the ticket is greater than 0. If it is greater, it enters the loop, then sleeps, and then simulates a decrease in the number of train tickets. Otherwise exit the loop.

thread.cc

#include<iostream>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<vector>
#include<assert.h>
#include<unistd.h>
#include<memory>
#include"mythread.hpp"
using namespace std;

int ticket=1000;
void* getticket(void*args)
{
    
    
    string username=static_cast<const char*> (args);
   while(true)
    {
    
    
        if(ticket>0)
        {
    
    
             usleep(1234);
        cout<<"User name:"<<username<<"get tickets ing..."<<"ticket num: "<<ticket<<endl;
                   ticket--;
        }else
        {
    
    
            break;//没票了退出循环
        }
        
    }
    return nullptr;
}

int main()
{
    
    
unique_ptr<thread> thread1(new thread(getticket,(void*)"user1",1));
unique_ptr<thread> thread2(new thread(getticket,(void*)"user2",2));
unique_ptr<thread> thread3(new thread(getticket,(void*)"user3",3));
thread1->join();
thread2->join();
thread3->join();
    return 0;
}

mythread.hpp

#include<iostream>
#include<pthread.h>
#include<string.h>
#include<functional>
using namespace std;


class thread;//声明
class Context
{
    
    
 public:
 thread* _this;//this指针
 void* _args;//函数参数
 public:
 Context()
 :_this(nullptr)
 ,_args(nullptr)
 {
    
    }
 ~Context()
 {
    
    }
};

class thread
{
    
    
public:
typedef  function<void* (void*)> func_t;//包装器构建返回值类型为void* 参数类型为void* 的函数类型
const int num=1024;
 thread(func_t func,void* args,int number=0)//构造函数
 : fun_(func)
 ,args_(args)
 {
    
    
char namebuffer[num];
snprintf(namebuffer,sizeof namebuffer,"threa--%d",number);//缓冲区内保存线程的名字即几号线程
Context* ctx=new Context();//
ctx->_this=this;
ctx->_args=args_;
int n=pthread_create(&pid_,nullptr,start_rontine,ctx);//因为调用函数start_rontine是类内函数,具有缺省参数this指针,在后续解包参数包会出问题,所以需要一个类来直接获取函数参数
assert(n==0);
(void)n;
 }

static void* start_rontine(void* args)
{
    
    
Context* ctx=static_cast<Context*>(args);
void *ret= ctx->_this->run(ctx->_args);//调用外部函数
delete ctx;
return ret;
}
void* run(void* args)
{
    
    
    return fun_(args);//调用外部函数
}
void join()
{
    
    
  int n=  pthread_join(pid_,nullptr);
  assert(n==0);
  (void)n;
}
~thread()
{
    
    
    //
}

    private:
    string name_;//线程的名字
    pthread_t pid_;//线程id
  func_t fun_;//线程调用的函数对象
  void* args_;//线程调用的函数的参数
};

image-20230714154928176

  • You can see that the number of votes will be negative.

Here’s why:

  • After the if statement determines that the condition is true, the code can be switched to other threads concurrently.
  • usleep is a process that simulates a long business process. In this long business process, many threads may enter this code segment.
  • As soon as the process comes in, it first determines whether the ticket is greater than 0. First, the ticket data in the memory is loaded into the CPU register, and then the judgment is made. At this time, the ticket is greater than 0. As soon as the thread enters the loop, then usleep sleeps. At this time, the thread is about to be shut down by the OS. Switch, the context data on the CPU is also switched away. Thread 2 and thread 3 also proceed in the same manner and both enter the loop.

image-20230714160946123

  • The ticket– operation itself is not an atomic operation, but corresponds to three assembly instructions:
  • load: Load the shared variable ticket from memory into the register

  • update: update the value in the register and perform -1 operation

  • store: Write the new value from the register back to the memory address of the shared variable ticket

  • As soon as the thread is switched again, load the context back to the CPU and then do ticket–. Load the ticket data in the memory into the CPU again, then do –, and then put the ticket data back into the memory, with ticket=0 at this time. Go to thread two and do the same work as thread one. After ticket-, the data is put back into the memory. At this time, ticket=-1. After the same operation of exiting thread three, ticket=-2. This is the case where ticket is a negative number.

To solve the above problems, you need to do the following:

  • The code must have mutually exclusive behavior: when the code enters the critical section for execution, other threads are not allowed to enter the critical section.
  • If multiple threads require the execution of code in a critical section at the same time, and no thread is executing in the critical section, only one thread can be allowed to enter the critical section.
  • If a thread is not executing in a critical section, then the thread cannot prevent other threads from entering the critical section.

These three points are essentially the concept of a mutex. The lock provided on Linux is called a mutex.

image-20230714183431846

mutex interface

Initialize mutex

static allocation
 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//静态分配,一般用于在全局定义
Dynamic allocation: pthread_mutex_init initializes the mutex

function prototype

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
  • mutex is a mutex that needs to be initialized, and the address of the mutex needs to be passed
  • attr is the attribute of the lock, usually set to nullptr
  • If the application is successful, 0 will be returned. If the application fails, an error code will be returned.

destroy mutex

int pthread_mutex_destroy destroy mutex

function prototype

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • mutex is a mutex that needs to be destroyed, and the address of the mutex needs to be passed
  • If the application is successful, 0 will be returned. If the application fails, an error code will be returned.

One note of caution:

  • PTHREAD_ MUTEX_ INITIALIZERMutexes initialized with do not need to be destroyed
  • Do not destroy a locked mutex
  • For a mutex that has been destroyed, make sure that no thread will try to lock it later.

Mutex locking and unlocking

pthread_mutex_lock lock

function prototype

 int pthread_mutex_lock(pthread_mutex_t *mutex);
  • mutex is a mutex that needs to be locked, and the address of the mutex needs to be passed
  • The mutex is in an unlocked state. This function will lock the mutex and return success.
  • If the lock is successful, 0 will be returned. If it fails, the specified mutex will be blocked and unlocked, and an error code will be returned.
  • When the function call is initiated, other threads have already locked the mutex, or there are other threads applying for the mutex at the same time, but there is no competition for the mutex, then the pthread_lock call will be blocked (the execution flow is suspended), waiting for the mutex. Unlocked. That is, the locking of this function is a blocking application.
pthread_mutex_trylock non-blocking application lock

function prototype

int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • mutex is a mutex that needs to be locked, and the address of the mutex needs to be passed

  • If the application is successful, 0 will be returned. If the application fails, an error code will be returned immediately.

pthread_mutex_unlockunlock

function prototype

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • mutex is the mutex that needs to be unlocked, and the address of the mutex needs to be passed

  • If the application is successful, 0 will be returned. If the application fails, an error code will be returned.

Modify the error code above

int ticket=1000;
pthread_mutex_t mut;//定义全局的互斥量
void* getticket(void*args)
{
    
    

    string username=static_cast<const char*> (args);
   while(true)
    {
    
    
        {
    
    
        pthread_mutex_lock(&mut);//阻塞式加锁
        if(ticket>0)
        {
    
    
             usleep(1234);
        cout<<"User name:"<<username<<"get tickets ing..."<<"ticket num: "<<ticket<<endl;
                   ticket--;
                   pthread_mutex_unlock(&mut);//解锁
        }else
        {
    
    
             pthread_mutex_unlock(&mut);//解锁
            break;//没票了退出循环
        }
        }//将临界区放进代码块内
        usleep(1000);//模拟产生订单
        
    }
    return nullptr;
}

int main()
{
    
    

pthread_mutex_init(&mut,nullptr);//初始化互斥量
unique_ptr<thread> thread1(new thread(getticket,(void*)"user1",1));
unique_ptr<thread> thread2(new thread(getticket,(void*)"user2",2));
unique_ptr<thread> thread3(new thread(getticket,(void*)"user3",3));
thread1->join();
thread2->join();
thread3->join();
pthread_mutex_destroy(&mut);//互斥量的销毁
    return 0;
}

About the concept of lock:

  • After locking critical resources, multiple execution streams are accessed serially, so the execution speed of the program is slower than the speed of concurrent execution.
  • Locking only stipulates that threads execute critical sections serially, and thread execution priority is determined by the competition results.
  • The locking process is safe, that is, the locking process is atomic.
  • When the thread that applied for the lock first is switched, the lock is also switched, and the other threads cannot apply successfully, and the shared area cannot continue to execute until the thread releases the lock.
  • When using locks, keep the granularity of the shared area as small as possible
  • For threads accessing critical resources, try to achieve lock consistency, either lock all threads, or none at all.

have to be aware of is:

  • After the above example, everyone has realized that simple i++ or ++i is not atomic, and there may be data consistency issues.
  • In order to implement mutex lock operations, most architectures provide swap or exchange instructions. The function of this instruction is to exchange data in registers and memory units. Since there is only one instruction, atomicity is guaranteed, even on multi-processor platforms. ,The bus cycles for accessing memory are also sequential. When the ,exchange instruction on one processor is executed, the ,exchange instruction on another processor can only wait for bus ,cycles. Now let’s change the pseudocode of lock and unlock
lock:
     mob $0,%al  //第一条指令
     xchgb,%al,mutex  //第二条指令
     if(al寄存器的内容>0)
     {
      return 0;
     }else
          挂起等待;
    goto lock;
 
unlock:
    movb $1,mutex   
    唤醒等待Mutex的线程;
    return 0;
  • When thread 1 applies for locking: the first instruction puts 0 into the al register in the CPU

image-20230714211146861

  • The second instruction: exchange the value of mutex in memory (here set to 1) with the value in the al register

image-20230714211225113

  • Then determine whether the value in the al register is greater than 0. If it is greater than 0, return 0, that is, the lock is successful. At this time, thread one has successfully locked the specified critical resource; if it is other results, the thread hangs and waits. Wait for the thread holding the lock to release the lock

  • If thread one completes one or two instructions at this time, the value in the al register is 1, and the value contained in mutex is 0. The OS switches thread one to thread two. When thread one is switched, the context of the CPU is also switched. After thread two is switched in, the first and second instructions are executed in the same way. First, 0 is set into the al register, and then the value of the al register is exchanged with the value contained in mutex. At this time, the value of mutex is 1, and then the mutex is judged. If the value is not greater than 0, it is judged to be false, and thread two needs to wait.

image-20230714214138895

C++ secondary encapsulation mutex

mutex.hpp

#include<iostream>
using namespace std;

class Mutex{
    
    
public:
Mutex(pthread_mutex_t * mutex=nullptr):mutex_(mutex){
    
    }
void Lock()
{
    
    
    if(mutex_)
    {
    
    
        pthread_mutex_lock(mutex_);//加锁
    }
    
}

void UnLock()
{
    
    
    if(mutex_)
    {
    
    
 pthread_mutex_unlock(mutex_);//解锁
    }
   
}
private:
pthread_mutex_t *mutex_;

};

class LockReady
{
    
    
    public:
    LockReady(pthread_mutex_t* mutex)
    :mutex_(mutex)
    {
    
    
mutex_.Lock();//加锁
    }
    ~LockReady()
    {
    
    
        mutex_.UnLock();//解锁
    }
    public:
  Mutex mutex_;
};

Thread safety

concept

  • Thread safety: When multiple threads run the same piece of code concurrently, different results will not occur. This problem often occurs when global variables or static variables are operated without lock protection.
  • Reentrancy: The same function is called by different execution flows. Before the current process has finished executing, other execution flows will enter again, which is called reentrancy. If a function is reentrant and the running results will not be any different or have any problems, then the function is called a reentrant function. Otherwise, it is a non-reentrant function.

Common thread unsafe situations

Functions that do not protect shared variables
Function status changes as the function is called
Function that returns a pointer to a static variable
Functions that call thread-unsafe functions

And common thread safety situations

Each thread only has read permissions on global variables or static variables, but no write permissions.
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-reentrant situations

The malloc/free function is called, because the malloc function uses a global linked list to manage the heap.
Standard I/O library functions are called. Many implementations of the standard I/O library use global data structures in a non-reentrant manner.
Reentrant function body uses static data structure

Common reentrancy situations

Do not use global or static variables
Do not use the space opened with malloc or new
Do not call non-reentrant functions
No static or global data is returned, all data is provided by the caller of the function
Use local data, or protect global data by making local copies of global data

So what is the relationship between reentrancy and thread safety?

  1. If a function is reentrant, then the thread calling the function is safe
  2. If the function is not reentrant, then the function cannot be used by multiple threads, otherwise it may cause thread safety issues
  3. If there is a global variable in a function, then the function may cause thread safety issues even if it is not a reentrant function.

What is the relationship between reentrant functions and thread safety?

  1. Reentrant functions are a type of thread-safe function
  2. Thread safety means that the function called is not necessarily a reentrant function, but the thread calling the reentrant function must be safe.
  3. 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 a deadlock, so it is not reentrant.

deadlock

deadlock concept

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 occupied by other processes and will not be released. In other words, if a thread holds resources but does not release them, applies for resources that cannot be applied for, and if the application fails and is in a blocking waiting state, then the resources held will not be released or used by other threads, and the thread will be in a deadlock state.

This concept is mostly used in multi-threaded scenarios. Will a single execution flow thread cause deadlock? The answer is yes. If a thread applies to lock the same resource twice in a row, the thread will be suspended. The first application is successful and the resource is successfully locked. The second application fails and is suspended until the lock is released. However, at this time, the resource has been locked by the thread itself and has not been released, so the thread is in a permanent waiting state.

The following is a case where a single execution flow causes a deadlock

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

using namespace std;

void* start_routine(void* args)
{
    
    
    long long * num=static_cast<long long*>(args);
    pthread_mutex_t mut;
    pthread_mutex_init(&mut,nullptr);//初始化锁
    pthread_mutex_lock(&mut);//加锁
    pthread_mutex_lock(&mut);//二次加锁
    cout<<"加锁的数据是num: "<<*num<<endl;
    pthread_mutex_unlock(&mut);//解锁
    return nullptr;
}
int main()
{
    
    
    pthread_t t1;
    long long num=199;
    pthread_create(&t1,nullptr,start_routine,(void*)num);
    pthread_join(t1,nullptr);
    return 0;
}

image-20230718170044135

  • The process of the thread that caused the deadlock is in a permanent waiting state, that is, the process is blocked in the waiting queue. We know that the process must be scheduled by the CPU. In other words, the process must occupy CPU resources. However, a CPU is only allowed to be occupied by one process at the same time, so these processes that schedule the CPU must be managed in a queue. In addition, the CPU is the fastest running speed, so this queue is called a running waiting queue and is in the running waiting queue. The process belongs to R state.
  • Similarly, processes that access other resources must be put into the queue. However, the speed of accessing hardware is slower. These processes must be put into the resource waiting queue to wait for access to resources. Then these processes belong to the S state, which is the blocked state. However, the OS has The code and data related to the blocked process may be put back to the disk, leaving only the task_struct in the queue, then these processes are suspended by the OS.

image-20230718172242033

To sum up:

  • From the perspective of the operating system, the process blocks and places the task_struct in the resource waiting queue. It can be said that the process is suspended and waiting.
  • From the user's perspective, the process is waiting to occupy resources and the program is stuck. It can be said that the process is suspended.
  • In fact, the lock can also be regarded as a software resource. When the process blocks and waits when applying for the lock, it can be regarded as the process entering the lock waiting resource queue.

Four conditions that cause deadlock

Mutually exclusive condition: a resource can only be used by one execution flow at a time
Request and hold conditions: When an execution flow is blocked due to requesting resources, the obtained resources are held on
Non-deprivation condition: The resources obtained by an execution flow cannot be forcibly deprived before they are used up.
Loop waiting condition: Several execution flows form a relationship of cyclic waiting for resources, starting from end to end.

How to avoid deadlock

  1. At least one of the four conditions causing deadlock is not met

  2. The locking order should be consistent . That is, multiple threads compete for the same resource at the same time. When a resource competes until it has not been released, other threads should block and wait, so that when the thread applies for other resources, it can release the resource and lock other resources.

  3. Avoid locks not being released

  4. Resources are allocated once. Prevent one thread from locking and unlocking different resources multiple times

  5. Algorithms to avoid deadlock can be used, such as deadlock detection method, banker's algorithm, etc.

Multi-thread synchronization

Synchronization concept

Thread synchronization: On the premise of ensuring data security, allowing threads to access critical resources in a specific order, thereby effectively avoiding the starvation problem, is called synchronization

Race conditions: Program exceptions caused by timing issues are called race conditions.

What needs to be clear is:

  • As a resource, locks are competitively occupied by threads. If there are some particularly competitive threads, the locks will always be occupied. The thread first applies for the lock and then unlocks it, and then applies for the lock and then unlocks it again. This will cause other threads to be unable to occupy the lock and cause starvation problems.
  • There is no problem with locking itself. After locking, only one thread will enter the critical area, which protects critical resources, but does not allow critical resources to be used efficiently.
  • However, under the concept of thread synchronization, after the thread that first occupies the lock is unlocked, it automatically goes to the end of the lock's resource waiting queue and performs blocking waiting, allowing each thread to access the lock in a specific order, that is, to access the critical section in sequence. Critical resources, avoiding the problem of starvation.

condition variable

Condition variable concept

  • Condition variables are used for waiting threads, usually used together with mutex locks.
  • A characteristic of a mutex is that it only has locking and non-locking, while condition variables allow threads to block and wait for another thread to send a signal to make up for the lack of the mutex.

condition variable function

  • The return value of the condition variable function is: 0 is returned if the call is successful, and an error code is returned if the call fails.
  • The condition variable is pthread_cond_t a type, and its essence is a structure . To simplify understanding, the implementation details can be ignored during application and simply treated as integers .

pthread_cond_init dynamically initializes condition variables

function prototype

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
  • The parameter cond is a condition variable. Here you need to pass the address of the condition variable.
  • attr is an attribute of the condition variable, usually set to empty
  • Dynamic initialization is usually used in local scope

Static initialization of condition variables

pthread_cond_t cond=PTHREAD_COND_INITIALIZER;//静态初始化条件变量—通常用在全局作用域
  • Note: Static initialized condition variables do not need to be destroyed

pthread_cond_wait blocks and waits for condition variables

function prototype

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
  • The parameter cond is a condition variable. Here you need to pass the address of the condition variable.
  • The parameter mutex is the mutex lock applied for by the thread that calls the condition variable. Here you need to pass the address of the mutex lock.
  • What the function does
  • First unlock the thread that calls the function (mutex), and then automatically wait at the end of the condition variable cond queue.
  • When receiving a signal from another thread, wake up the thread, unblock when the function returns, and reapply to acquire the mutex lock (mutex)

wake wait

There are two functions for wake-up waiting

pthread_cond_signal wakes up the first thread in the waiting queue
int pthread_cond_signal(pthread_cond_t *cond);
  • The parameter cond is a condition variable. Here you need to pass the address of the condition variable.
pthread_cond_broadcast wakes up all threads in the waiting queue
int pthread_cond_broadcast(pthread_cond_t *cond);
  • The parameter cond is a condition variable. Here you need to pass the address of the condition variable.

pthread_cond_destroy destroys condition variables

function prototype

int pthread_cond_destroy(pthread_cond_t *cond);
  • The parameter cond is a condition variable. Here you need to pass the address of the condition variable.

Here I use the ticket grabbing system section for demonstration. When there is no condition variable

#include<iostream>
#include<pthread.h>
#include<vector>
#include<assert.h>
#include<string.h>
#include<unistd.h>

using namespace std;
#define NUM 5
pthread_mutex_t mut=PTHREAD_MUTEX_INITIALIZER;//全局初始化互斥量
int ticket=1000;
void* start_routine(void* args)
{
    
    
    string str=static_cast<const char*>(args);

    while(true)
    {
    
    
         pthread_mutex_lock(&mut);//加锁
         if(ticket>0)
    {
    
    
        cout<<str<<"-> ticket: "<<ticket<<endl;
        ticket--;
    }else
    {
    
    
     pthread_mutex_unlock(&mut);//解锁
     break;
    }       
      pthread_mutex_unlock(&mut);//解锁
    }

}
int main()
{
    
    
    pthread_t dt[NUM];//数组存储多个线程id
    for(int i=0;i<NUM;i++)
    {
    
    
        char * name=new char[64];
        snprintf(name,64,"thread:%d",i);//设置线程名字
       int n= pthread_create(&dt[i],nullptr,start_routine,(void*)name);
    assert(n==0);
    }
   
   while(true)
   {
    
    
    usleep(500000);//1-1000-1000000
    cout<<"main thread wake up one thread"<<endl;
   }

   for(int i=0;i<NUM;i++)
   {
    
    
    pthread_join(dt[i],nullptr);//回收线程
   }
    return 0;
}
  • The main thread creates 5 new threads and lets the new threads complete the work of grabbing tickets. The lock is locked before grabbing the ticket, so only one thread enters the critical section at a time, and then unlocks the ticket and then cycles again, allowing all threads to compete for the same mutex lock. There will be a thread with strong competitiveness that has been doing the job of grabbing tickets, causing other threads to have starvation problems.

image-20230719111239072

Add condition variable

#include<iostream>
#include<pthread.h>
#include<vector>
#include<assert.h>
#include<string.h>
#include<unistd.h>

using namespace std;
#define NUM 5
pthread_mutex_t mut=PTHREAD_MUTEX_INITIALIZER;//全局初始化互斥量
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;//全局初始化条件变量
int ticket=1000;
void* start_routine(void* args)
{
    
    
    string str=static_cast<const char*>(args);

    while(true)
    {
    
    
         pthread_mutex_lock(&mut);//加锁
         if(ticket>0)
    {
    
    
 pthread_cond_wait(&cond,&mut);//让调用函数线程在条件变量出阻塞等待
        //到这里说明在条件变量处阻塞等待的线程已经被唤醒
        cout<<str<<"-> ticket: "<<ticket<<endl;
        ticket--;
    }else
    {
    
    
     pthread_mutex_unlock(&mut);//解锁
     break;
    }       
      pthread_mutex_unlock(&mut);//解锁
    }

}


int main()
{
    
    
    pthread_t dt[NUM];//数组存储多个线程id
    for(int i=0;i<NUM;i++)
    {
    
    
        char * name=new char[64];
        snprintf(name,64,"thread:%d",i);//设置线程名字
       int n= pthread_create(&dt[i],nullptr,start_routine,(void*)name);
    assert(n==0);
    }
   
   while(true)
   {
    
    
    usleep(500000);//1-1000-1000000
    pthread_cond_signal(&cond);//给条件变量发信号,唤醒处在条件变量处的阻塞的一个线程
    pthread_cond_broadcast(&cond);给条件变量发信号,唤醒处在条件变量处的阻塞的全部线程
    cout<<"main thread wake up one thread"<<endl;
   }

   for(int i=0;i<NUM;i++)
   {
    
    
    pthread_join(dt[i],nullptr);//回收线程
   }
    return 0;
}
  • After adding the condition variable, after the thread successfully applies for lock and enters the critical section, due to pthread_cond_waitthe function, the new thread first unlocks and then waits for the main thread to send a signal in the condition variable. The main thread sends the signal to the new thread. After the new thread receives the signal, it first adds lock, and then execute the code in the critical section. Unlocked later. Competitive locking is performed again. After entering the critical section, due to pthread_cond_waitthe function, the new thread is first unlocked and then automatically blocked and waited at the end of the condition variable queue.
  • Since the last thread that entered the critical section will automatically block and wait at the end of the condition variable queue, you will see that the threads grabbing tickets show a certain order. There will be no starvation problem in which a highly competitive thread keeps competing for the mutex lock to enter the critical section, causing other threads to be unable to access critical resources.

image-20230719122706978

Why pthread_cond_wait needs a mutex

  • Conditional waiting is a means of synchronization between threads. If there is only one thread and the condition is not satisfied, it will not be satisfied if it keeps waiting. Therefore, one thread must change the shared variable through certain operations to make the original unsatisfied condition become must be satisfied, and friendly notifications are made to threads waiting on the condition variable.
  • The conditions will not suddenly become satisfied for no reason, and will inevitably involve changes in the shared data. So it must be protected with a mutex lock. Without a mutex, shared data cannot be safely obtained and modified.
  • When entering the critical section, a lock must be locked first. Because the conditions are not met, the thread needs to be suspended. When the thread is suspended, it must be suspended with the lock. However, if the conditions are not met, the lock will not be released, causing a deadlock problem.
  • Therefore, pthread_cond_waitthe function needs to pass in a mutex to automatically release the lock when the thread is suspended, avoiding the deadlock problem. After the conditions are met, the thread is awakened and the code in the critical section can be executed later. At this time, the thread needs to be re-added. Lock

Wrong execution logic

As mentioned earlier, pthread_cond_waitthe function first suspends the thread and releases the lock, and the second function automatically locks the thread after it is awakened. Then we can also let the thread enter the critical section and release the lock first, and then let the thread pass the function pthread_cond_wait. Suspended waiting.

// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
    
    
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);

  • In fact, after unlocking and before waiting, other threads may obtain the mutex and meet the conditions. The phread_cond_signalfunction sends a signal to the thread. At this time, the suspended thread will miss the signal, which may eventually cause the thread to take forever. will not be woken up, so unlocking and waiting must be an atomic operation.
  • After actually entering pthread_cond_waitthe function, it will first determine whether the condition variable is equal to 0. If it is equal to 0, it means it is not satisfied. At this time, the corresponding mutex will be unlocked first, and then the condition variable will be changed to pthread_cond_wait1 when the function returns, and the corresponding The mutex is locked.

Condition variable usage specifications

  • Wait condition code
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
  • Send signal code to condition
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

Guess you like

Origin blog.csdn.net/m0_71841506/article/details/131806708