• Linux线程同步(下)


    前言

    在这里插入图片描述

    本篇博客紧接上一篇Linux线程同步(上),接着来介绍Linux中的线程同步。抓紧,发车啦!

    POSIX信号量

    POSIX信号量是一种线程同步机制,定义在POSIX标准中,可用于多种Unix-like操作系统,包括Linux。它们用于协调多个线程或进程之间对共享资源的访问,以避免竞态条件和数据不一致。POSIX信号量包括两种主要类型:

    • 命名信号量:这些信号量具有全局唯一的标识符,通常通过名字来引用。多个进程可以在不同时间打开和使用这些信号量,因此它们通常用于进程间通信(IPC)。命名信号量允许不同进程在不同地址空间中协调对共享资源的访问。

    • 未命名信号量:也被称为线程信号量,这些信号量只能在同一进程内的不同线程之间共享。它们通常用于线程同步,用于协调线程对共享数据的访问。

    POSIX信号量的主要操作包括创建、初始化、等待、释放和销毁信号量。线程或进程可以等待信号量以获取访问共享资源的权限,当访问完成后,释放信号量,以便其他线程或进程可以获得访问权限。POSIX信号量提供了一种强大的方式来确保多线程或多进程程序的正确性,同时允许有效地管理资源的访问。在多线程和多进程编程中,使用POSIX信号量可以帮助避免竞态条件,提高程序的可靠性。

    POSIX信号量的创建、初始化、等待、释放和销毁操作的返回值都是整数类型,用于指示函数执行的结果。如果成功销毁了信号量,它会返回0。如果发生错误,返回一个非零的错误码,表示销毁失败的原因。

    初始化信号量

    初始化信号量可以使用 sem_init 函数,sem_init 用于创建一个新的信号量或初始化一个现有信号量。这个函数通常在C或C++中使用,并在头文件中定义。它的原型如下:

    在这里插入图片描述

    该函数参数 sem 是一个指向要初始化的信号量的指针。pshared 参数用于指定信号量的共享性质,通常设置为0,表示信号量在进程内部共享,即线程间共享。value 参数是初始的信号量值,通常用于指定资源的初始可用数量。

    销毁信号量

    当不再需要这个信号量时,就需要将信号量进行销毁来清理资源。通常在不再需要信号量时,可以调用 sem_destroy 来确保资源被正确释放。其原型如下,参数 sem 是一个指向要销毁的信号量的指针。

    在这里插入图片描述

    等待信号量

    sem_wait 函数用于等待信号量的值减少,以便获取访问共享资源的许可。当信号量的值大于0时,sem_wait 函数将信号量的值减一,并继续执行。当信号量的值等于0时,sem_wait 函数将阻塞当前线程,直到信号量的值大于0为止。这表示其他线程可能已经占用了共享资源,需要等待资源可用。函数原型如下,参数 sem 是指向信号量的指针。

    在这里插入图片描述

    等待信号量的操作也可以称为P操作

    发布信号量

    sem_post 函数用于增加信号量的值,当调用 sem_post 时,信号量的值会增加一。这表示一个资源已经被释放,其他等待这个资源的线程可以继续执行。通常,sem_post 会与 sem_wait 配合使用,以实现多线程之间的同步。函数原型如下,其中参数 sem 是指向信号量的指针。

    在这里插入图片描述

    发布信号量的操作也可以称为V操作

    基于环形队列的生产消费模型

    在Linux线程同步(上)里写的生产者消费者的例子是基于queue的,阻塞队列的空间可以动态分配,现在可以基于固定大小的环形队列重写生产消费模型。模型使用一个循环队列作为共享缓冲区,生产者向队列中添加数据,而消费者从队列中取出数据,如下代码:

    #pragma once
    
    #include 
    #include 
    #include 
    
    static const int N = 5;
    
    template <class T>
    class RingQueue
    {
    public:
        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)
            : _cap(num), _ring(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)
        {
            p(_space_sem);
            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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    #include "RingQueue.hpp"
    #include "Task.hpp"
    #include 
    #include 
    
    using namespace std;
    
    void *comsumer(void *args)
    {
        RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
        while (true)
        {
            int data;
            rq->pop(&data);
            usleep(3200);
            std::cout << pthread_self() << " | consumer data: " << data << std::endl;
        }
    }
    void *productor(void *args)
    {
        RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
        std::string opers = "+-*/%";
        while (true)
        {
            int data = rand() % 10 + 1;
            std::cout << pthread_self() << " | productor data: " << data << std::endl;
            rq->push(data);
            sleep(1);                
        }
    }
    
    int main()
    {
        RingQueue<int> *rq = new RingQueue<int>;
        pthread_t c, p;
    
        pthread_create(&c, nullptr, comsumer, rq);
        pthread_create(&p, nullptr, productor, rq);
    
        pthread_join(c, nullptr);
        pthread_join(p, nullptr);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    当消费者速度大于生产者时:

    在这里插入图片描述
    当生产者速度大于消费者时:

    在这里插入图片描述

    线程池

    线程池是一种多线程处理方式,它在程序启动时就会创建一定数量的线程,并将它们保存在一个线程池中。这些线程可以在需要的时候被重复使用,而不需要反复创建和销毁,从而提高了程序的性能和响应速度。其主要优势和特点包括:

    • 资源管理: 线程池能够管理可用的系统资源,防止过多线程同时运行,避免资源耗尽。

    • 性能提升: 由于线程的创建和销毁是昂贵的操作,线程池能够避免这种开销,提高了程序的性能。

    • 控制并发度: 线程池可以限制并发线程的数量,防止系统被过多线程拥塞,确保系统稳定性。

    • 任务队列: 线程池通常与任务队列结合使用,将需要执行的任务放入队列中,线程池中的线程会从队列中取出任务并执行。

    • 灵活性: 可以通过控制线程池的大小和任务队列的大小来调整程序的处理能力,使其适应不同的系统负载。

    而典型的线程池通常包含这几个要素,如线程池管理器,常用于创建、销毁和管理线程池中的线程。工作队列,常用于存储待执行的任务,线程会从队列中取出任务进行处理。线程池,包含多个线程的集合,用于执行工作队列中的任务。任务,包含需要执行的具体操作单元,通常以函数或类的形式存在。

    在使用线程池时,开发者只需要将任务放入工作队列中,线程池会自动管理线程的执行和任务的调度,提高了程序的可维护性和可扩展性。

    线程池封装

    在封装线程池之前,可以先封装一个计算器任务类,用户输入后将计算的结果返回出来,如下代码:

    #pragma once
    #include 
    #include 
    
    class Task
    {
    public:
        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 = -2;
                else
                    _result = _x % _y;
            }
            break;
            default:
                break;
            }
        }
        std::string formatRes()
        { 
            return std::to_string(_result) + "(" + std::to_string(_exitCode) + ")";
        }
        ~Task()
        {
        }
    private:
        int _x;
        int _y;
        char _op;
    
        int _result;
        int _exitCode;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    代码中创建了一个 Task 类,构造函数用于初始化 _x、_y、_op、_result 和 _exitCode 成员变量,然后定义了 operator() 函数,用于执行算术运算。在 operator() 函数中,根据给定的运算符 _op,它执行相应的运算,并将结果存储在 _result 中。如果是除法或取余运算,还会检查除数是否为零,并将错误码存储在 _exitCode 中。formatRes() 函数返回一个字符串,表示任务的结果和可能的错误码。它将 _result 和 _exitCode 转换为字符串,用括号括起来。

    创建一个线程池对象后,通过 pushTask 方法将任务加入队列,线程池会自动将任务分发给空闲的线程来执行,而线程在执行完任务后会等待新任务。如下代码:

    #pragma once
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "Task.hpp"
    const static int N = 5;
    
    template<class T>
    class ThreadPool
    {
    public:
        ThreadPool(int num = N) : _num(num), _threads(num)
        {
            pthread_mutex_init(&_lock, nullptr);
            pthread_cond_init(&_cond, nullptr);
        }
        void LockQueue()
        {
            pthread_mutex_lock(&_lock);
        }
        void UnLockQueue()
        {
            pthread_mutex_unlock(&_lock);
        }
        void ThreadWait()
        {
            pthread_cond_wait(&_cond, &_lock);
        }
        void ThreadWakeup()
        {
            pthread_cond_signal(&_cond);
        }
        bool isEmpty()
        {
            return _tasks.empty();
        }
        T popTask()
        {
            T t = _tasks.front();
            _tasks.pop();
            return t;
        }
    
        static void *threadRoutine(void* args)
        {
            ThreadPool<T>* tp = static_cast<ThreadPool<T>*>(args);
            
            pthread_detach(pthread_self());
            while(1)
            {
                tp->LockQueue();
                while(tp->isEmpty())
                {
                    pthread_cond_wait(&tp->_cond, &tp->_lock);
                }
                T t = tp->popTask();         
                tp->UnLockQueue();
                t(); //运行任务
                std::cout << "pthread : " << pthread_self() << " result : " << t.formatRes() << std::endl; 
            }
        }
        void start()
        {
            for(int i = 0; i < _num; i++)
            {
                pthread_create(&_threads[i], nullptr, threadRoutine, this);
            }
        }
        void pushTask(const T& s)
        {
            LockQueue();
            _tasks.push(s);
            ThreadWakeup();   
            UnLockQueue();    
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&_lock);
            pthread_cond_destroy(&_cond);
        }
    private:
        std::vector<pthread_t> _threads;
        int _num;
        std::queue<T> _tasks;
        pthread_mutex_t _lock;
        pthread_cond_t _cond;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    在这里插入图片描述

    总结

    文章介绍了多线程同步中的POSIX信号量,对信号量的初始化、销毁、等待和发布函数进行相关介绍,最后使用信号量实现了简单的基于环形队列的生产消费模型,最后结合多线程的互斥与同步实现了一个简单版的线程池的封装。确保正确的互斥和同步是多线程编程的挑战之一,合理使用互斥锁、条件变量和其他同步工具可以确保多线程程序的正确性和稳定性,因此掌握多线程的同步与互斥对于多线程编程是非常重要的。码文不易,如果文章对你有帮助的话,就来一个👍呗。

    在这里插入图片描述

  • 相关阅读:
    【MySQL习题】各个视频的平均完播率【全网最详细教学】
    千万支持补助让人心动?成都市关于加快发展先进制造业实现工业转型升级发展若干政策的意见
    P1529 [USACO2.4] 回家 Bessie Come Home 题解
    Spring Boot OAuth 2.0整合—高级配置
    (免费分享)基于springboot健康运动-带论文
    2.2.5 SQL注入(盲注)
    Crazy Binary String
    车载信息安全
    T31快启图像效果优化
    高级深入--day40
  • 原文地址:https://blog.csdn.net/wzh18907434168/article/details/133791375