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

2024-05-07 05:20

本文主要是介绍信号量线程池读者写者模型,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

大家好,我是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);

和生产者消费者的区别

读者写者模型

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

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

生产者消费者模型

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

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

这篇关于信号量线程池读者写者模型的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/966432

相关文章

一份LLM资源清单围观技术大佬的日常;手把手教你在美国搭建「百万卡」AI数据中心;为啥大模型做不好简单的数学计算? | ShowMeAI日报

👀日报&周刊合集 | 🎡ShowMeAI官网 | 🧡 点赞关注评论拜托啦! 1. 为啥大模型做不好简单的数学计算?从大模型高考数学成绩不及格说起 司南评测体系 OpenCompass 选取 7 个大模型 (6 个开源模型+ GPT-4o),组织参与了 2024 年高考「新课标I卷」的语文、数学、英语考试,然后由经验丰富的判卷老师评判得分。 结果如上图所

大语言模型(LLMs)能够进行推理和规划吗?

大语言模型(LLMs),基本上是经过强化训练的 n-gram 模型,它们在网络规模的语言语料库(实际上,可以说是我们文明的知识库)上进行了训练,展现出了一种超乎预期的语言行为,引发了我们的广泛关注。从训练和操作的角度来看,LLMs 可以被认为是一种巨大的、非真实的记忆库,相当于为我们所有人提供了一个外部的系统 1(见图 1)。然而,它们表面上的多功能性让许多研究者好奇,这些模型是否也能在通常需要系

人工和AI大语言模型成本对比 ai语音模型

这里既有AI,又有生活大道理,无数渺小的思考填满了一生。 上一专题搭建了一套GMM-HMM系统,来识别连续0123456789的英文语音。 但若不是仅针对数字,而是所有普通词汇,可能达到十几万个词,解码过程将非常复杂,识别结果组合太多,识别结果不会理想。因此只有声学模型是完全不够的,需要引入语言模型来约束识别结果。让“今天天气很好”的概率高于“今天天汽很好”的概率,得到声学模型概率高,又符合表达

智能客服到个人助理,国内AI大模型如何改变我们的生活?

引言 随着人工智能(AI)技术的高速发展,AI大模型越来越多地出现在我们的日常生活和工作中。国内的AI大模型在过去几年里取得了显著的进展,不少独创的技术点和实际应用令人瞩目。 那么,国内的AI大模型有哪些独创的技术点?它们在实际应用中又有哪些出色表现呢?此外,普通人又该如何利用这些大模型提升工作和生活的质量和效率呢?本文将为你一一解析。 一、国内AI大模型的独创技术点 多模态学习 多

OpenCompass:大模型测评工具

大模型相关目录 大模型,包括部署微调prompt/Agent应用开发、知识库增强、数据库增强、知识图谱增强、自然语言处理、多模态等大模型应用开发内容 从0起步,扬帆起航。 大模型应用向开发路径:AI代理工作流大模型应用开发实用开源项目汇总大模型问答项目问答性能评估方法大模型数据侧总结大模型token等基本概念及参数和内存的关系大模型应用开发-华为大模型生态规划从零开始的LLaMA-Factor

模型压缩综述

https://www.cnblogs.com/shixiangwan/p/9015010.html

AI赋能天气:微软研究院发布首个大规模大气基础模型Aurora

编者按:气候变化日益加剧,高温、洪水、干旱,频率和强度不断增加的全球极端天气给整个人类社会都带来了难以估计的影响。这给现有的天气预测模型提出了更高的要求——这些模型要更准确地预测极端天气变化,为政府、企业和公众提供更可靠的信息,以便做出及时的准备和响应。为了应对这一挑战,微软研究院开发了首个大规模大气基础模型 Aurora,其超高的预测准确率、效率及计算速度,实现了目前最先进天气预测系统性能的显著

PyTorch模型_trace实战:深入理解与应用

pytorch使用trace模型 1、使用trace生成torchscript模型2、使用trace的模型预测 1、使用trace生成torchscript模型 def save_trace(model, input, save_path):traced_script_model = torch.jit.trace(model, input)<

Java线程面试题(50)

不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题。Java语言一个重要的特点就是内置了对并发的支持,让Java大受企业和程序员的欢迎。大多数待遇丰厚的Java开发职位都要求开发者精通多线程技术并且有丰富的Java程序开发、调试、优化经验,所以线程相关的问题在面试中经常会被提到。 在典型的Java面试中, 面试官会从线程的基本概念问起, 如:为什么你需要使用线程,

关于文章“python+百度语音识别+星火大模型+讯飞语音合成的语音助手”报错的修改

前言 关于我的文章:python+百度语音识别+星火大模型+讯飞语音合成的语音助手,运行不起来的问题 文章地址: https://blog.csdn.net/Phillip_xian/article/details/138195725?spm=1001.2014.3001.5501 1.报错问题 如果运行中报错,且报错位置在Xufi_Voice.py文件中的pcm_2_wav,如下图所示