信号量&线程池&读者写者模型

前言

大家好,我是jiantaoyab,本篇文章接着给大家介绍线程有关的信号量及线程池的基本理解。

信号量

在计算机中,信号量就是个 0 以上的整数值,当为 0 时表示己无可用信号 ,或者说条件不再允许,因此它表示某种信号的累积“ 量飞故称为信号量。

信号量是种同步机制。同步一般是指合作单位之间为协作完成某项工作而共同遵守的工作步调,强调的是配合时序,就像十字路口的红绿灯,只有在绿灯亮起的情况下司机才能踩油门把车往前开,这就是一种同步,同步简单来说就是不能随时随意工作,工作必须在某种条件具备的情况下才能开始,工作条件具备的时间顺序就是时序。

信号量就是个计数器,它的计数值是自然数,用来记录所积累信号的数量。

既然信号量是计数也必然要有对计数增减的方法。P、V 操作来表示信号量的减、增,这两个都是荷兰语中的单词的缩写。P是指Proberen表示减少, V是指单词 Verhogen,表示增加。

V操作

  1. 将信号量的值加 1
  2. 唤醒在此信号量上等待的线程

P操作:

  1. 判断信号量是否大于 0 。
  2. 若信号量大于 0,则将信号量减 1 。
  3. 若信号量等于 0,当前线程将自己阻塞,以在此信号量上等待。

信号量是个全局共享变量,up 和 down 又都是读写这个全局变量的操作,而且它们都包含一系列的子操作,因此它们必须都是原子操作。
信号量的初值代表是信号资源的累积量,也就是剩余量,若初值为1的话,它的取值就只能为0和1,这便称为二元信号量,我们可以利用二元信号量来实现锁。

在二元信号量中,down 操作就是获得锁,up操作就是释放锁。我们可以让线程通过锁进入临界区,可以借此保证只有一个线程可以进入临界区,从而做到互斥。

大致流程为:

  1. 线程 A 进入临界区前先通过 down 操作获得锁(我们有强制通过锁进入临界区的手段),此时信号量的值便为 0 。
  2. 后续线程 B 再进入临界区时也通过 down 操作获得锁,由于信号量为 0,线程 B 便在此信号量上等待,也就是相当于线程 B 进入了睡眠态
  3. 当线程 A 从临界区出来后执行 up 操作释放锁,此时信号量的值重新变成 1 ,之后线程 A 将线程 B唤醒 。
  4. 线程 B 醒来后获得了锁,进入临界区 。

信号量接口

初始化

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享 value:信号量初始值

基本操作

//销毁
int sem_destroy(sem_t *sem);
//等待信号量
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
//发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1int sem_post(sem_t *sem);//V()

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

ring_queue.hpp

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

using namespace std;
namespace ns_ring_queue
{
    template <class T>
    class RingQueue
    {
    private:
        vector<T> _ring_queue; // 环形队列
        int _cap;              // 容量
        sem_t _black_sem;      // 生产者关系空位置资源
        sem_t _data_sem;       // 消费者关心数据资源
        int _c_step;           // 消费者走到哪
        int _p_step;
        pthread_mutex_t _c_mtx;
        pthread_mutex_t _p_mtx;

    public:
        RingQueue(int cap = 3)
            : _ring_queue(cap), _cap(cap)
        {
            sem_init(&_black_sem, 0, cap);
            sem_init(&_data_sem, 0, 0);
            _c_step = _p_step = 0; // 从0开始走

            pthread_mutex_init(&_c_mtx, nullptr);
            pthread_mutex_init(&_p_mtx, nullptr);
        }
        ~RingQueue()
        {
            sem_destroy(&_black_sem);
            sem_destroy(&_data_sem);
            pthread_mutex_destroy(&_c_mtx);
            pthread_mutex_destroy(&_p_mtx);

        }
    public:
        void Push(const T& in)
        {
            //生产接口
            sem_wait(&_black_sem); //并行的先分配好信号量

            pthread_mutex_lock(&_p_mtx);//再分配锁

            _ring_queue[_p_step]=in;//生产到p_step位置上
            _p_step++;
            _p_step%=_cap;

            pthread_mutex_unlock(&_p_mtx);

            sem_post(&_data_sem);
        }

         void Pop(T* out)
        {
            //消费接口
            sem_wait(&_data_sem);
            pthread_mutex_lock(&_c_mtx);

            *out=_ring_queue[_c_step]; //拿c_step上的数据
            _c_step++;
            _c_step%=_cap;

            pthread_mutex_unlock(&_c_mtx);

            sem_post(&_black_sem);
        }

    };
}

ring_cp.cc

#include"ring_queue.hpp"
#include<pthread.h>
#include<time.h>
#include<unistd.h>

using namespace ns_ring_queue;

void* consumer(void* args)
{
     RingQueue<int>* rq = (RingQueue<int>*)args;
     while(true){
         int data = 0;
         rq->Pop(&data);
         std::cout << "消费数据是: " << data << std::endl;
        //  sleep(1);
     }
}

void* producter(void* args)
{
     RingQueue<int>* rq = (RingQueue<int>*)args;
     while(true){
         int data = rand()%20 + 1;
         std::cout << "生产数据是:  " << data << std::endl;
         rq->Push(data);
        sleep(1);
     }
}
int main()
{
    srand((long long)time(nullptr));
    RingQueue<int>* rq = new RingQueue<int>();

    pthread_t c0,c1,c2,c3,p0,p1,p2;

    pthread_create(&c0, nullptr, consumer, (void*)rq);
    pthread_create(&c1, nullptr, consumer, (void*)rq);
    pthread_create(&c2, nullptr, consumer, (void*)rq);
    pthread_create(&c3, nullptr, consumer, (void*)rq);
    pthread_create(&p0, nullptr, producter, (void*)rq);
    pthread_create(&p1, nullptr, producter, (void*)rq);
    pthread_create(&p2, nullptr, producter, (void*)rq);

    pthread_join(c0, nullptr);
    pthread_join(c1, nullptr);
    pthread_join(c2, nullptr);
    pthread_join(c3, nullptr);
    pthread_join(p0, nullptr);
    pthread_join(p1, nullptr);
    pthread_join(p2, nullptr);
    return 0;
}

线程的阻塞和唤醒

线程能运行是因为调度器将线程从就绪队列中摘出来放到处理器上,如果不让线程在就绪队列出现就能实现线程的阻塞。阻塞是线程自己发出的动作,也就是线程自己阻塞自己,并不是被别人阻塞的,阻塞是线程主动的行为。己阻塞的钱程是由别人来唤醒的,唤醒是被动的。

当线程被换上处理器运行后,在其时间片内,线程将主宰自己的命运。阻塞是一种意愿,表达的是线程运行中发生了一些事情,这些事情通常是由于缺乏了某些运行条件造成的,以至于线程不得不暂时停下来,必须等到运行的条件再次具备时才能上处理器继续运行。因此,阻塞发生的时间是在线程自己的运行过程中,是线程自己阻塞自己,并不是被谁阻塞。

己被阻塞的线程是无法运行的,属于睡梦中,因此它只能让别人唤醒它,否则它永远没有运行的机会。这个别人便是锁的持有者,它释放了锁之后便去唤醒在它后面因获取该锁而阻塞的线程。

因此唤醒己阻塞的线程是由别的线程,通常是锁的持有者来做的。值得注意的是线程阻塞是线程执行时的“动作”,因此线程的时间片还没用完,在唤醒之后,线程会继续在剩余的时间片内运行,调度器并不会将该线程的时间片“充满”,也就是不会再用线程的优先级priority 为时间片 ticks 赋磕。

因为阻塞是线程主动的意愿,它也是“迫于无奈”才“慷慨”地让出处理器资源给其他线程,所以调度器没必要为其“大方”而“赏赐”它完整的时间片。

线程池

线程池就是事先创建若干个可执行的线程放入一个池中,需要的时候从池中取线程而不用自行创建,使用完毕后不用销毁线程而是返回池中,从而减少线程对象创建和销毁的开销。这种做法可以大大提高服务器的性能,因为线程的创建和销毁成本相对较高,而线程池通过复用线程来降低这种开销。

线程池的应用场景

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

线程池的种类

  1. FixedThreadPool(定长线程池)
    • 线程数量固定,不会随着任务的增加而增加。
    • 任务队列为链表结果的有界队列。
    • 适用于任务量较稳定、预计执行时间较长的情况。
  2. CachedThreadPool(可缓存线程池)
    • 线程数量不固定,根据任务的数量和执行时间自动调整。
    • 任务队列为不存储元素的阻塞队列。
    • 适用于执行大量、耗时少的任务。
    • 线程闲置超过60秒会被回收。
  3. SingleThreadExecutor(单线程化线程池)
    • 只有一个核心线程,无非核心线程。
    • 任务队列为链表结果的有界队列。
    • 适用于需要保证任务顺序执行的场景。
  4. ScheduledThreadPool(定时线程池)
    • 核心线程数量固定,非核心线程数量无限。
    • 任务队列为延时阻塞队列。
    • 执行定时或周期性任务。
    • 线程闲置超过10秒会被回收。
  5. ForkJoinPool(分治任务线程池)
    • 将任务拆分成更小的子任务,并行执行,并合并结果。
    • 适用于处理大规模的计算任务。
  6. WorkStealingPool(工作窃取线程池)
    • 每个线程都有自己的工作队列。
    • 当某个线程完成自己的任务后,会从其他线程的队列中偷取任务来执行。
    • 这种机制可以提高任务执行效率。

普通版本的线程池

thread_pool.hpp

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

namespace ns_threadpool
{
    template<class T>
    class ThreadPool
    {
    private:
        int _num;
        std::queue<T> _task_queue; //临界资源
        pthread_mutex_t _mtx;
        pthread_cond_t _cond;

    public:

        void Lock()
        {
            pthread_mutex_lock(&_mtx);
        }
        void Unlock()
        {
            pthread_mutex_unlock(&_mtx);
        }
        void Wait()
        {
            pthread_cond_wait(&_cond,&_mtx);
        }
        void Wakeup()
        {
            pthread_cond_signal(&_cond);
        }
        bool IsEmpty()
        {
            return _task_queue.empty();
        }

    public:

        ThreadPool(int num=3)
            :_num(num)
            {
                pthread_mutex_init(&_mtx,nullptr);
                pthread_cond_init(&_cond,nullptr);
            } 
        
        //想要线程在类中执行类中的函数是不可以行的,因为只能传一个参数
        //要设置成static,让线程执行静态方法
        static void* Rountine(void* args)
        {
            pthread_detach(pthread_self());//分离不用等
            ThreadPool<T> *tp=(ThreadPool<T> *)args; //this指针
            while(true)
            {
                tp->Lock();
                while(tp->IsEmpty())
                {
                    tp->Wait(); //没任务等待
                }
                
                int data = 0;
                tp->PopTask(&data);

                tp->Unlock();
                //处理任务
                std::cout << "消费数据是: " << data << std::endl;

               
            }
        }

        void InitThreadPool()
        {
            pthread_t id;
            for(int i=0;i<3;i++)
            {
                pthread_create(&id,nullptr,Rountine,(void*)this);
            }
        }

        void PushTask(const T& in)
        {
            Lock();
            _task_queue.push(in);
            Unlock();
            Wakeup();
        }

        void PopTask(T* out)
        {
          
            *out=_task_queue.front();
            _task_queue.pop(); 

        }

        ~ThreadPool()
        {
            pthread_mutex_destroy(&_mtx);
            pthread_cond_destroy(&_cond);
        }
    };
}

main.cc

#include"thread_pool.hpp"
#include<ctime>
#include<cstdlib>

using namespace ns_threadpool;


int main()
{
    ThreadPool<int>* tp= new ThreadPool<int>(3);
    tp->InitThreadPool();
    srand((long long)time(nullptr));
    while(true)
    {
        sleep(1);
        tp->PushTask(rand()%5);
    }
}

单例模式

单例模式(Singleton Pattern)是一种创建型设计模式,它确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

一般我们有2种方法实现单例模式,饿汉模式和懒汉模式。

饿汉式:全局的单例实例在类装载时构建,急切实例化。这种方式比较简单,因为单例的实例被声明为static和final变量,第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

template <typename T>
class Singleton {
	static T data;
public:
	static T* GetInstance()
    {
		return &data;
	}
};

懒汉式:懒汉式是类加载进内存的时候,并不立即初始化这个单例,只有在第一次调用getInstance()方法时才初始化。需要加上双重检查锁定保证线程安全。

//存在线程安全问题
template <typename T>
class Singleton {
	static T* inst;
public:
	static T* GetInstance() {
	if (inst == NULL) 
    {
		inst = new T();
	}
	return inst;
	}
};

懒汉下单例模式线程池

thread_pool.hpp

#pragma once

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

namespace ns_threadpool
{
    const int g_num = 5;

    template <class T>
    class ThreadPool
    {
    private:
        int num_;
        std::queue<T> task_queue_; //该成员是一个临界资源

        pthread_mutex_t mtx_;
        pthread_cond_t cond_;

        static ThreadPool<T> *ins;

    private:
        // 构造函数必须得实现,但是必须的私有化
        ThreadPool(int num = g_num) : num_(num)
        {
            pthread_mutex_init(&mtx_, nullptr);
            pthread_cond_init(&cond_, nullptr);
        }
        ThreadPool(const ThreadPool<T> &tp) = delete;
        //赋值语句
        ThreadPool<T> &operator=(ThreadPool<T> &tp) = delete;

    public:
        static ThreadPool<T> *GetInstance()
        {
            static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
            // 当前单例对象还没有被创建
            if (ins == nullptr) //双判定,减少锁的争用,提高获取单例的效率!
            {
                pthread_mutex_lock(&lock);
                if (ins == nullptr)
                {
                    ins = new ThreadPool<T>();
                    ins->InitThreadPool();
                    std::cout << "首次加载对象" << std::endl;
                }
                pthread_mutex_unlock(&lock);
            }

            return ins;
        }

        void Lock()
        {
            pthread_mutex_lock(&mtx_);
        }
        void Unlock()
        {
            pthread_mutex_unlock(&mtx_);
        }
        void Wait()
        {
            pthread_cond_wait(&cond_, &mtx_);
        }
        void Wakeup()
        {
            pthread_cond_signal(&cond_);
        }
        bool IsEmpey()
        {
            return task_queue_.empty();
        }

    public:
        // 在类中要让线程执行类内成员方法,是不可行的!
        // 必须让线程执行静态方法
        static void *Rountine(void *args)
        {
            pthread_detach(pthread_self());
            ThreadPool<T> *tp = (ThreadPool<T> *)args;

            while (true)
            {
                tp->Lock();
                while (tp->IsEmpey())
                {
                    tp->Wait();
                }
                //该任务队列中一定有任务了
                T t;
                tp->PopTask(&t);
                tp->Unlock();

                t();
            }
        }
        void InitThreadPool()
        {
            pthread_t tid;
            for (int i = 0; i < num_; i++)
            {
                pthread_create(&tid, nullptr, Rountine, (void *)this /*?*/);
            }
        }
        void PushTask(const T &in)
        {
            Lock();
            task_queue_.push(in);
            Unlock();
            Wakeup();
        }
        void PopTask(T *out)
        {
            *out = task_queue_.front();
            task_queue_.pop();
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&mtx_);
            pthread_cond_destroy(&cond_);
        }
    };

    template <class T>
    ThreadPool<T> *ThreadPool<T>::ins = nullptr;
} 

main.cc

#include "thread_pool.hpp"
#include "Task.hpp"

#include <ctime>
#include <cstdlib>

using namespace ns_threadpool;
using namespace ns_task;

int main()
{
    std::cout << "当前正在运行我的进程其他代码..." << std::endl;
    std::cout << "当前正在运行我的进程其他代码..." << std::endl;
    std::cout << "当前正在运行我的进程其他代码..." << std::endl;
    std::cout << "当前正在运行我的进程其他代码..." << std::endl;
    std::cout << "当前正在运行我的进程其他代码..." << std::endl;
    std::cout << "当前正在运行我的进程其他代码..." << std::endl;
    std::cout << "当前正在运行我的进程其他代码..." << std::endl;

    sleep(5);
    srand((long long)time(nullptr));
    while(true)
    {
        sleep(1);

        //网络
        Task t(rand()%20+1, rand()%10+1, "+-*/%"[rand()%5]);
        ThreadPool<Task>::GetInstance()->PushTask(t);
        //单例本身会在任何场景,任何环境下被调用
        //GetInstance():被多线程重入,进而导致线程安全的问题
        std::cout << ThreadPool<Task>::GetInstance() << std::endl;
    }

    return 0;
}

读者写者模型

读者写者模型是操作系统中的一种同步与互斥机制,它与消费者和生产者模型有相似之处,但也有其独特的特点。在读者写者模型中,主要涉及到两种角色:读者和写者。

读者:在读者写者模型中,读者是指那些只需要读取数据的角色。多个读者之间可以同时读取数据,不会发生冲突,因此读者之间是并行的关系。

写者:写者是指那些需要修改数据的角色。由于数据在修改时不能被其他写者或读者访问,因此写者之间、以及写者与读者之间是互斥的关系。

读者写者模型的特点

  1. 多读少写:在多数应用中,读者的数量通常远多于写者。读者写者模型适用于这种多读少写的情况,能够有效地提高数据的并发访问性能。
  2. 读者并行:多个读者可以同时访问数据,实现并行读取,提高了数据的访问效率。
  3. 写者互斥:当有写者需要修改数据时,其他写者和读者都不能访问数据,保证了数据的一致性和完整性。
  4. 优先级策略:根据不同的应用场景,可以设定不同的优先级策略,如读者优先、写者优先或公平策略等。

读写锁接口

设置读写优先

默认是读锁优先级高

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);

pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和
PTHREAD_RWLOCK_PREFER_READER_NP 一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
//初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t
*restrict attr);

//销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

//加锁和解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

和生产者消费者的区别

读者写者模型

读者:只读取数据,不进行写操作。

写者:对数据进行修改或写入。

生产者消费者模型

  • 生产者:负责生成数据并放入缓冲区。
  • 消费者:从缓冲区取出数据进行处理。

读者和消费者最大的区别就是读者只是读并不会对数据进行取走处理。读者写者模型主要关注于如何协调读者和写者对共享数据的并发访问,而生产者消费者模型则主要解决生产者与消费者之间的数据传递和同步问题。

  • 36
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值