[Linux] Thread synchronization and mutual exclusion

1. Thread mutual exclusion

1. Related concepts

1. Critical resources: Resources shared by multi-threaded execution flows, and resources that can only be accessed by one execution flow at a time are called critical resources. (Multi-thread, multi-process printing data)

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

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

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

Implement a small example

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

using namespace std; int ticket=10000;

void* threadRoutinue(void* args) {
     
     
    const char* name=static_cast<const char*>(args);
    while(true)
    {
     
     
        if(ticket>0)
        {
     
     
            usleep(1000);//模拟抢票花费时间
            cout<<name<<"get a ticket: "<<ticket--<<endl;
        }
        else{
     
     
            break;
        }
    }
    return nullptr; }

int main() {
     
     
    //创建线程模拟抢票
    pthread_t tid[4];
    int n=sizeof(tid)/sizeof(tid[0]);
    for(int i=0;i<n;i++)
    {
     
     
        char buffer[64];
        snprintf(buffer,sizeof(buffer),"thread_%d",i);
        pthread_create(tid+i,nullptr,threadRoutinue,buffer);
    }

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

    return 0; } 

Insert image description here

As you can see from the program, when the number of votes reaches 0, there are no more votes, and the thread should exit.

But in the result, the number of votes was even negative. What's going on?

Here is a question, is the access to tickets (critical resources) atomic? (Is it safe?) The answer is definitely not! !
Insert image description here
It may be that in a thread A, just after the tickets are loaded into the memory, thread A is cut away. At this time, the data and context of thread A are saved, and thread A is stripped from the CPU.

Thread B starts to grab tickets. If he is very competitive, he grabs 1,000 tickets after one run.

After thread B finishes executing, thread A comes again. It will continue execution from where it was last executed. However, the data of tickets it saved last time was 10,000, so after grabbing a ticket, it writes back the remaining 9,999 tickets. Memory, originally there were 9,000 votes left after thread B finished executing, but after thread A finished executing, the number of remaining votes increased.

2. Mutex lock (mutex)

For the above ticket grabbing program, if you want each thread to grab tickets correctly, you must ensure that when a thread enters the ticket grabbing process, other threads cannot grab tickets.
Therefore, you can add a mutex lock to the ticket grabbing process.

pthread_mutex_init, pthread_mutex_destroy: Initialize and destroy thread locks

#include <pthread.h> 
// pthread_mutex_t mutex: 锁变量,所有线程都可看到 
int pthread_mutex_destroy(pthread_mutex_t *mutex);// 销毁锁 
int pthread_mutex_init(pthread_mutex_t *restrict mutex,constpthread_mutexattr_t *restrict attr);// 初始化锁 
// attr: 锁属性,我们传入空指针就可  
// 如果将锁定义为静态或者全局的,可以使用宏直接初始化,且不用销毁 
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock, int pthread_mutex_unlock: lock and unlock threads

#include <pthread.h> 
int pthread_mutex_lock(pthread_mutex_t *mutex);   
int pthread_mutex_unlock(pthread_mutex_t *mutex); 

Lock the small demo for grabbing tickets

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

using namespace std; int ticket=10000;  //临界资源
pthread_mutex_t mutex;

void* threadRoutinue(void* args) {
     
     
    const char* name=static_cast<const char*>(args);
    while(true)
    {
     
     
        pthread_mutex_lock(&mutex);
        if(ticket>0)
        {
     
     
            usleep(1000);//模拟抢票花费时间
            cout<<name<<" get a ticket: "<<ticket--<<endl;
            pthread_mutex_unlock(&mutex);
        }
        else{
     
     
            cout<<name<<"票抢完了"<<endl;
            pthread_mutex_unlock(&mutex);
            break;
        }
        usleep(1000);
    }
    return nullptr; }

int main() {
     
     
    pthread_mutex_init(&mutex,nullptr);
    //创建线程模拟抢票
    pthread_t tid[4];
    int n=sizeof(tid)/sizeof(tid[0]);
    for(int i=0;i<n;i++)
    {
     
     
        char buffer[64];
        snprintf(buffer,sizeof(buffer),"thread_%d",i);
        pthread_create(tid+i,nullptr,threadRoutinue,buffer);
    }

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

    pthread_mutex_destroy(&mutex);    
    return 0; } 

Multithread critical resource atomic
Insert image description here

Details:
1. All threads that access the same critical resource must be locked and protected, and the same lock must be added. This is a rule, there can be no exceptions. 2.
Before each thread accesses a critical resource, it must be locked. The essence of locking is to lock the critical section, and the locking granularity should be as fine as possible.
3. When a thread accesses the critical section, it needs to lock it first -> so all threads must see the same lock -> the lock itself is a public resource -> how does the lock ensure its own security? -> Locking and unlocking are inherently atomic.
4. The critical section can be a line of code or a batch of code. a. The thread may be switched? Of course it’s possible b. Will switching have any impact? No, because after a thread applies for a lock, the thread is temporarily switched. No other thread can enter the critical section and cannot apply for the lock, so it cannot access critical resources.
5. This is also the manifestation of serialization brought about by mutual exclusion. From the perspective of other threads, the status that is meaningful to other threads is: the lock is applied for (the lock is held), the lock is released (the lock is not held) lock), atomicity.

3. Principle of mutex lock

Taking the ticket grabbing program as an example, when a thread needs to access critical resources, it needs to access mtx first. In order for all threads to see it, the lock must be global.

And the lock itself is also a critical resource. So how to ensure that the lock itself is safe, that is, the process of obtaining the lock is safe.
The principle is: the process of locking and unlocking is atomic!

So what counts as atomic: after a line of code is translated into assembly, there is only one assembly, which is atomic.

To implement mutex lock operations, most architectures provide swap or exchange instructions.

The function of this instruction is to exchange data in the register and memory unit. Since there is only one instruction, atomicity is guaranteed.

Even on a multi-processor platform, bus cycles for accessing memory are sequential. When a swap instruction on one processor is executed, the swap instruction on another processor can only wait for bus cycles.

Insert image description here

After the thread applies for the lock, it enters the critical section to access critical resources. At this time, the thread may also be cut away. After being cut away, the context will be protected, and the lock data is also in the context.

Therefore, the lock is also taken away, so even if the thread is suspended, other threads cannot apply for the lock or enter the critical section.

You must wait for the thread that owns the lock to release the lock before you can apply for the lock.

4. Customize a lock

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

//封装锁

class _Mutex
{
    
    
public:
    _Mutex(pthread_mutex_t* mutex):_mutex(mutex)
    {
    
    }

    void lock()
    {
    
    
        pthread_mutex_lock(_mutex);
    }

    void unlock()
    {
    
    
        pthread_mutex_unlock(_mutex);
    }
private:
    pthread_mutex_t* _mutex;
};


class lockGuard
{
    
    
public:
    lockGuard(pthread_mutex_t* mutex):_mutex(mutex)
    {
    
    
        _mutex.lock();
    }

    ~lockGuard()
    {
    
    
        _mutex.unlock();
    }

private:
    _Mutex _mutex;
};

We can use our own encapsulated lock to solve the ticket grabbing problem
Insert image description here

2. Reentrancy and thread safety

Thread safety: Thread safety refers to the situation in multi-threaded programming where multiple threads compete for access to critical resources without causing data ambiguity or program logic confusion. 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. We call it reentrancy. If a function is reentrant and the running results will not be any different or have any problems, the function is called a reentrant function. Otherwise, it is a non-reentrant function.

Thread safety is achieved through synchronization and mutual exclusion

Specific mutual exclusion can be achieved through mutex locks and semaphores, and synchronization can be achieved through condition variables and semaphores.

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

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

Reentrancy and thread safety:

  • A function is reentrant, that is, it is thread-safe.

  • The function is not reentrant, so it cannot be used by multiple threads, which may cause thread safety issues.

  • If there are global variables in a function, then the function is neither thread-safe nor reentrant.

The difference between reentrancy and thread safety:

  • Reentrant functions are a type of thread-safe function

  • Thread safety does not necessarily mean reentrancy, but reentrant functions must be thread safe.

  • If access to critical resources is locked, this function is thread-safe, but if the lock of this reentrant function has not been released, a deadlock will occur.

3. 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.

Four necessary conditions for 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

Core idea: Any one of the four necessary conditions to break deadlock!

  • Unlocked

  • Actively release lock

  • Apply for locks in order

  • One-time allocation of resources

A small demo to break the deadlock (actively releasing the lock)

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

using namespace std;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *pthreadRoutinue(void *args) {
     
       
    pthread_mutex_lock(&mutex); //加锁
    cout<<"I get a mutex"<<endl;

    pthread_mutex_lock(&mutex); //产生死锁
    cout<<"i alive again"<<endl;

    return nullptr; }

int main() {
     
     
    pthread_t pid;
    pthread_create(&pid, nullptr, pthreadRoutinue, nullptr);

    sleep(3);
    cout<<"main thread run"<<endl;

    pthread_mutex_unlock(&mutex);//主线程区解锁
    cout<<"main thread unlock"<<endl;

    sleep(3);
    return 0; } 

Insert image description here

4. Thread synchronization

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.

1.Conditional variables

concept

Unlike mutex locks, condition variables are used for waiting rather than locking. Condition variables are used to automatically block a thread until a special situation occurs. Usually condition variables and mutex locks are used together.

Condition variables allow us to sleep and wait for a certain condition to occur. Condition variables are a mechanism that uses global variables shared between threads for synchronization. It mainly includes two actions: one thread waits for "the condition of the condition variable to be true" and hangs; the other thread makes "the condition is true" (given that the condition is true) Signal).

Condition variable interface

pthread_cond_init, pthread_cond_destroy: initialize and destroy condition variables

#include <pthread.h> 
int pthread_cond_destroy(pthread_cond_t *cond);
// pthread_cond_t:条件变量类型,类似pthread_mutex_t int
pthread_cond_init(pthread_cond_t *restrict cond,constpthread_condattr_t *restrict attr);   

// 如果是静态或全局的条件变量可使用宏初始化:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 

pthread_cond_wait, pthread_cond_signal: waiting conditions, waking up threads

#include <pthread.h>   

// 等待条件满足 
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);   
// 唤醒一个线程,在cond等待队列里的第一个线程 
int pthread_cond_signal(pthread_cond_t *cond);
// 一次唤醒所有线程 
int pthread_cond_broadcast(pthread_cond_t *cond); ```

demo

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

using namespace std;
#define num 5

int ticket =1000; pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

void *active(void *args) {
     
       
    string name=static_cast<const char*>(args);
    while(true)
    {
     
     
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex); //调用该函数,会自己释放锁
        cout<<name<<" 活动"<<endl;
        pthread_mutex_unlock(&mutex);
    }
    return nullptr; }

int main() {
     
     
    pthread_t tids[num];
    for(int i=0;i<num;i++)
    {
     
     
        char * name=new char[64];
        snprintf(name,64,"thread-%d",i); //线程创
        pthread_create(tids+i,nullptr,active,name);
    }

    sleep(3);
    while(true)
    {
     
     
        cout<<"main thread wakeup thread..."<<endl;
        //pthread_cond_signal(&cond); //唤醒cond队列中的一个线程
        pthread_cond_broadcast(&cond); //将cond队列中所以线程唤醒
        sleep(1);
    }
    for(int i=0;i<num;i++)
    {
     
     
        pthread_join(tids[i],nullptr); //线程等待
    }

    sleep(3);
    return 0; } 

Insert image description here

Implement producer-consumer model based on blocking queue

Insert image description here

The producer-consumer pattern solves the strong coupling problem between producers and consumers through a container .

Producers and consumers do not communicate directly with each other, but communicate through blocking queues. Therefore, after the producers produce the data, they do not need to wait for the consumers to process it, but directly throw it to the blocking queue. The consumers do not ask the producers for data, but Take it directly from the blocking queue. The blocking queue is equivalent to a buffer, balancing the processing capabilities of producers and consumers. This blocking queue is used to decouple producers and consumers.

Advantages of the producer-consumer model: decoupling supports concurrency and supports uneven busyness.

In fact, the pipeline communication in inter-process communication mentioned before is a producer-consumer model. The pipeline allows different processes to see the same resource. , and the pipeline has its own synchronization and mutual exclusion mechanism. The essence of inter-process communication is actually the producer-consumer model.

Code:

blockQueue.hpp

#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>

const int gcap = 5;

template <class T> class BlockQueue {
     
      public:
    BlockQueue(const int cap = gcap) : _cap(cap)
    {
     
     
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_consumerCond, nullptr);
        pthread_cond_init(&_productorCond, nullptr);
    }

    ~BlockQueue()
    {
     
     
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_consumerCond);
        pthread_cond_destroy(&_productorCond);
    }

    bool isFull()
    {
     
     
        return _cap == _q.size();
    }

    void push(const T &in)
    {
     
      // 生产
        pthread_mutex_lock(&_mutex);
        while (isFull())  //细节1:使用while ,防止多线程被唤醒生产过多
        {
     
        // 我们只能在临界区内部,判断临界资源是否就绪 注定了我们在当前一定是持有锁的
            pthread_cond_wait(&_productorCond, &_mutex); // 如果队列为满,生产者线程休眠 ,此时持有锁,wait会将锁unlock
            // 当线程醒来的时候,注定了继续从临界区内部继续运行,因为是在临界区被切走的
            // 注定了当线程被唤醒的时候,继续在pthread_cond_wait()函数继续向后运行,又要重新申请锁,申请成功才会彻底返回
        }
        // 没有满,让他继续生产
        _q.push(in);
        //策略,唤醒消费者线程
        pthread_cond_signal(&_consumerCond);
        pthread_mutex_unlock(&_mutex);
    }
    void pop(T *out)
    {
     
     
        pthread_mutex_lock(&_mutex);
        while (_q.empty())  //队列为空
        {
     
     
            pthread_cond_wait(&_consumerCond, &_mutex); 
        }

        *out = _q.front();
        _q.pop();
        //策略,唤醒生产者
        pthread_cond_signal(&_productorCond);
        pthread_mutex_unlock(&_mutex);
    }

private:
    std::queue<T> _q;
    int _cap;
    pthread_mutex_t _mutex;
    pthread_cond_t _consumerCond;  // 消费者对应的条件变量 空 wait
    pthread_cond_t _productorCond; // 生产者对应的条件变量 满 wait }; ```

task.hpp

#pragma once

#include <iostream>
#include <string>

class Task {
     
      public:
    Task() {
     
     }
    Task(int x, int y, char op) : _x(x), _y(y), _op(op), _result(0), _exitCode(0)
    {
     
     
    }

    void operator()()
    {
     
     
        switch (_op)
        {
     
     
        case '+':
            _result = _x + _y;
            break;
        case '-':
            _result = _x - _y;
            break;
        case '*':
            _result = _x * _y;
            break;
        case '/':
            if(_y==0) _exitCode=-1;
            else _result = _x / _y;
            break; 
        case '%':
            if(_y==0) _exitCode=-1;
            else _result = _x % _y;
            break;
        default:
            break;
        }
    }

    std::string formatArg()
    {
     
     
        return std::to_string(_x)+' '+ _op+ ' '+std::to_string(_y)+" = ";
    }
    std::string formatRes()
    {
     
     
        return std::to_string(_result) + "(" +std::to_string(_exitCode)+")";
    }
    ~Task(){
     
     }

private:
    int _x;
    int _y;
    char _op;

    int _result;
    int _exitCode; }; ```

main.cc

#include "blockQueue.hpp"
#include"task.hpp"
#include<ctime>
#include<unistd.h>

void *consumer(void *args) {
     
     
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
    while(true)
    {
     
     
        sleep(1);
        Task t;
        //1.将数据从blockqueue中获取  -- 获取到数据
        bq->pop(&t);
        t();
        //2.结合某种业务逻辑,处理数据!
        std::cout<<"consumer data: "<<t.formatArg()<<t.formatRes()<<std::endl;
    } }

void *productor(void *args) {
     
     
    srand((uint64_t)time(nullptr)^getpid());
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
    std::string opers="+-*/%";
    while(true)
    {
     
     
        //1.先通过某种渠道获取数据
        int x=rand()%20+1;
        int y=rand()%10+1;
        //2.将数据推送到blockqueue  -- 完成生产过程
        char op=opers[rand()%opers.size()];
        Task t(x,y,op);
        bq->push(t);
        std::cout<<"productor Task: "<<t.formatArg()<<"?"<<std::endl;
    } }

int main() {
     
     
    //BlockQueue<int> *bq = new BlockQueue<int>();
    BlockQueue<Task> *bq = new BlockQueue<Task>();
    // 单生产,单消费  支持多生产,多消费,因为看到同一快资源,使用同一把锁
    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, productor, bq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);

    delete bq;
    return 0; } ```

operation result:

Insert image description here

2. Signal amount

concept

The semaphore is essentially a counter, used to describe the number of critical resources in the critical section.

If critical resources can be divided into smaller resources, and if handled properly, it is possible for multiple threads to access critical resources at the same time, thereby achieving concurrency.

But each thread that wants to access critical resources must first apply for semaphore resources.

Semaphore operation interface

When the application for the semaphore is successful, the number of critical resources will be reduced by one; when the semaphore is released, the number of critical resources will be increased by one.

Since semaphores are used to maintain critical resources, they must first ensure their safety, so conventional ++ or - operations on global variables are definitely not possible.

P operation (apply for semaphore)
V operation (release semaphore)

sem_init, sem_destroy: initialize and destroy the semaphore (the specific usage is very similar to mutex and cond)

#include <semaphore.h> 
int sem_init(sem_t *sem, int pshared, unsigned int value); 
// pshared: 默认为0, value:信号量的初始值(count) 
int sem_destroy(sem_t *sem);   
// sem_t :信号量类型 // Link with -pthread. ```

sem_wait, sem_signal: apply for and release semaphores

int sem_wait(sem_t *sem); // P操作  
int
sem_post(sem_t *sem); // V操作    
// Link with -pthread. ```

Producer-consumer model based on ring queue

Insert image description here
However, the current empty and full judgment of the ring queue is no longer judged by the two methods in the previous chapter, because there is a semaphore to judge.

When the queue is empty, the consumer and producer point to the same location. (Production and consumption threads cannot be performed at the same time) (Producer execution)

When the queue is full, consumers and producers also point to the same location. (Production and consumption threads cannot be performed at the same time) (Consumer execution)

When the queue is neither empty nor full, the consumer and producer do not point to the same location. (Production and consumption threads can execute concurrently)

According to the above three situations, the producer-consumer model based on the ring queue should comply with the following rules:

  • Producers cannot trap consumers in a trap

  • Consumers cannot exceed producers

  • When pointing to the same location, it is necessary to determine who to execute first based on the empty and full status.

  • In other cases, consumers and producers can execute concurrently

accomplish:

ringQueue.hpp

#pragma once

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

static const int N = 5;

template <class T> class RingQueue {
     
      private:
    void P(sem_t &s)  
    {
     
     
        sem_wait(&s);
    }
    void V(sem_t &s)
    {
     
     
        sem_post(&s);
    }
    void Lock(pthread_mutex_t &m)
    {
     
     
        pthread_mutex_lock(&m);
    }
    void Unlock(pthread_mutex_t &m)
    {
     
     
        pthread_mutex_unlock(&m);
    }

public:
    RingQueue(int num = N) : _ring(num), _cap(num)
    {
     
     
        sem_init(&_data_sem, 0, 0);
        sem_init(&_space_sem, 0, num);
        _c_step = _p_step = 0;

        pthread_mutex_init(&_c_mutex, nullptr);
        pthread_mutex_init(&_p_mutex, nullptr);
    }
    // 生产
    void push(const T &in)
    {
     
     
        // 1. 可以不用在临界区内部做判断,就可以知道临界资源的使用情况
        // 2. 什么时候用锁,对应的临界资源,是否被整体使用
        P(_space_sem);  // P() 
        Lock(_p_mutex); 
        _ring[_p_step++] = in;
        _p_step %= _cap;
        Unlock(_p_mutex);
        V(_data_sem);
    }
    // 消费
    void pop(T *out)
    {
     
     
        P(_data_sem);
        Lock(_c_mutex);
        *out = _ring[_c_step++];
        _c_step %= _cap;
        Unlock(_c_mutex);
        V(_space_sem);
    }
    ~RingQueue()
    {
     
     
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);

        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
    }

private:
    std::vector<T> _ring;
    int _cap;         // 环形队列容器大小
    sem_t _data_sem;  // 只有消费者关心
    sem_t _space_sem; // 只有生产者关心
    int _c_step;      // 消费位置
    int _p_step;      // 生产位置

    pthread_mutex_t _c_mutex;
    pthread_mutex_t _p_mutex; };


task.hpp

#pragma once

#include <iostream>
#include <string>

class Task {
     
      public:
    Task() {
     
     }
    Task(int x, int y, char op) : _x(x), _y(y), _op(op), _result(0), _exitCode(0)
    {
     
     
    }

    void operator()()
    {
     
     
        switch (_op)
        {
     
     
        case '+':
            _result = _x + _y;
            break;
        case '-':
            _result = _x - _y;
            break;
        case '*':
            _result = _x * _y;
            break;
        case '/':
            if(_y==0) _exitCode=-1;
            else _result = _x / _y;
            break; 
        case '%':
            if(_y==0) _exitCode=-1;
            else _result = _x % _y;
            break;
        default:
            break;
        }
    }

    std::string formatArg()
    {
     
     
        return std::to_string(_x)+' '+ _op+ ' '+std::to_string(_y)+" = ";
    }
    std::string formatRes()
    {
     
     
        return std::to_string(_result) + "(" +std::to_string(_exitCode)+")";
    }
    ~Task(){
     
     }

private:
    int _x;
    int _y;
    char _op;

    int _result;
    int _exitCode; }; ```

main.cc

#include "ringQueue.hpp"
#include "task.hpp"
#include <ctime>
#include <pthread.h>
#include <memory>
#include <sys/types.h>
#include <unistd.h>
#include <cstring>

using namespace std;

const char *ops = "+-*/%";

void *consumerRoutine(void *args) {
     
     
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
    while (true)
    {
     
     
        Task t;
        rq->pop(&t);
        t();
        cout << "consumer done, 处理完成的任务是: " << t.formatRes() << endl;
    } }

void *productorRoutine(void *args) {
     
     
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
    while (true)
    {
     
     
        // sleep(1);
        int x = rand() % 100;
        int y = rand() % 100;
        char op = ops[(x + y) % strlen(ops)];
        Task t(x, y, op);
        rq->push(t);
        cout << "productor done, 生产的任务是: " << t.formatArg() << endl;
    } }

int main() {
     
     
    srand(time(nullptr) ^ getpid());
    RingQueue<Task> *rq = new RingQueue<Task>();
    // 单生产单消费
    // pthread_t c, p;
    // pthread_create(&c, nullptr, consumerRoutine, rq);
    // pthread_create(&p, nullptr, productorRoutine, rq);

    // pthread_join(c, nullptr);
    // pthread_join(p, nullptr);
    //多生产,多消费
    pthread_t c[3], p[2];
    for (int i = 0; i < 3; i++)
        pthread_create(c + i, nullptr, consumerRoutine, rq);
    for (int i = 0; i < 2; i++)
        pthread_create(p + i, nullptr, productorRoutine, rq);

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

        pthread_join(c[i], nullptr);
    for (int i = 0; i < 2; i++)

        pthread_join(p[i], nullptr);

    delete rq;
    return 0; } ```



operation result

Insert image description here

5. Summary

Similarities and differences between mutex locks and semaphores

  • Mutexes are locked by the same thread, and semaphores can be operated by different threads for PV.

  • Counting semaphores allow multiple threads, and the value is the number of remaining available resources. Mutex locks guarantee mutually exclusive access to a shared resource by multiple threads, and semaphores are used to coordinate access by multiple threads to a series of resources.

Similarities and differences between condition variables and semaphores

  • Using condition variables can wake up all waiters at once, but this semaphore does not have this function.

  • The semaphore has a value, but the condition variable does not. From an implementation perspective, a semaphore can be implemented using mutex + count + cond. Because the semaphore has a state and can be synchronized accurately, the semaphore can solve the problem of wake-up loss in condition variables.

  • Condition variables generally need to be used with mutex locks, while semaphores can be used according to the situation.

  • The reason why semaphores are also provided with mutexes and condition variables is: Although the purpose of semaphores is to synchronize between processes, and the purpose of mutexes and condition variables is to synchronize between threads, semaphores can also be used between threads, mutexes Locks and condition variables can also be used between processes. The most useful scenario for semaphores is to indicate the amount of available resources.

Guess you like

Origin blog.csdn.net/Tianzhenchuan/article/details/133210063
Recommended