生产者消费者模型
生产者与消费者模型:大佬们针对典型的应用场景设计的解决方案
生产者与消费者模型应用场景:有线程不断地产生数据,有线程不断地处理数据。
数据的产生与数据的处理,如果放在同一个线程中完成,因为执行流只有一个,那么肯定是生产一个处理一个,处理完一个后才能生成一个。
这样的依赖关系太强了,如果处理的比较慢,就会把程序的速度拖慢下来。
因此将生产与处理放在不同的执行流中完成,那么生产和处理的线程如何实现通信呢?
中间增加一个数据缓冲区(队列),作为中间的数据缓冲场所。
产生线程只负责将数据放入缓冲区,处理数据只负责将数据从缓冲区中取出并处理。
生产者消费者模型的优点
-
解耦合
将模块分开,耦合度降低两边都会比较灵活,各自可以按照各自的业务压力创建线程处理,
且处理模块代码发生改变只需要改变处理模块代码。 -
支持忙闲不均
缓冲区大小可以一直申请,是可调的,数据多了就可以放在缓冲区。
-
支持并发
可以有多个执行流进行处理
生产者消费者模型的优点很显然,生活中很多时候是请求多于响应的,根据这个模型,大佬们就设计了线程池。
线程池
一个或多个创建好的线程+线程安全pcb等待队列
其他线程将要处理的任务,添加到线程池任务队列中,线程池的线程不断从pcb等待队列中获取任务进行处理。
应用:应用于有大量请求任务需要处理的场景
优点:
避免了峰值压力下资源耗尽的风险
避免了大量频繁创建与销毁线程带来的时间成本
正常流程:任务处理T = 创建线程T1 + 任务处理过程 + 销毁线程T3
这样的流程大量的时间都放在了创建和销毁线程,没有把所有的资源都放在任务处理上
实现:一个或多个创建好的线程 + 线程安全的任务队列
如何知道任务如何处理:
1.在线程入口函数中直接定义好任务处理逻辑
错误的理解
这样写死的线程池只能处理特定的功能,达不到处理各式各样功能的目的
2.添加任务的线程,添加任务是,将要处理的数据和数据处理的方法一并传入线程池
线程池中的线程只需要调用处理函数传入数据即可
task{
void* data;
void(*handler)(void* data);
}
线程池中的线程只需要调用一下 handler(&data);就可以完成处理数据完成任务
设计一个简单的线程池
设计任务类:
当线程池的线程获取到任务对象时,只需要调用一下它的Run即可完成任务
//定义一个函数指针
typedef void(*handler_t)(int)
class ThreadTask
{
private:
int _data; //要传入的数据
handler_t _handler; //处理数据的方法
public:
void Run()
{
_handler(data);
}
}
设计一个线程安全的任务 队列:
主要就是实现同步与互斥,这点可以使用信号量,可以使用条件变量都可以。
本文以条件变量实现为例:
#define MAXCAP 3
class BlockQueue
{
private:
std::queue<ThreadTask> q;
size_t cap;
pthread_mutex_t mutex;
pthread_cond_t Cus_cond;
pthread_cond_t Pro_cond;
public:
BlockQueue(size_t c = MAXCAP):cap(c)
{
//mutex=PTHREAD_MUTEX_INITIALIZER; //C++不用这个
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&Cus_cond,NULL);
pthread_cond_init(&Pro_cond,NULL);
}
~BlockQueue()
{
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&Cus_cond);
pthread_cond_destroy(&Pro_cond);
}
bool push(const ThreadTask& data)
{
//要往缓冲区队列放数据,就得先判断缓冲区是否满了,所有线程都可以访问缓冲区,那么缓冲区就是临界资源
//需要进行保护
pthread_mutex_lock(&mutex);
//循环条件防止时间片调度产生的访问出错
while(q.size() == cap)
{
pthread_cond_wait(&Pro_cond,&mutex);//解锁阻塞加锁
}
q.push(data);
pthread_cond_signal(&Cus_cond);
pthread_mutex_unlock(&mutex);
return true;
}
bool pop(ThreadTask* data)
{
pthread_mutex_lock(&mutex);
while(q.empty() == true)
{
pthread_cond_wait(&Cus_cond,&mutex);
}
*data= q.front();
q.pop();
pthread_cond_signal(&Pro_cond);
pthread_mutex_unlock(&mutex);
return true;
}
};
可以自行实现一个,加上模板最好。
信号量版本
点击超链接,我在另一篇文章写到过,不再赘述。
实现一个线程池类
线程池类只需要创建几个线程不停地获取任务即可,所以
成员设计:
1.最大线程数 -- 决定创建几个线程 _max_thread
2.一个线程安全的任务队列, BlockQueue
成员方法:
3.其他线程将任务入队,TaskPush(const ThreadTask& task);
4.构造方法,创建指定数量的线程
5.线程入口函数,出队获取任务,调用task.Run()
#define MAX_THREAD 5
#define MAX_QUEUE 10
class threadpool
{
private:
int _max_thread;
BlockQueue _queue;
private:
//这个入口函数如果在类中声明,就会自动带有this指针,而入口函数的参数返回值是固定的,将他定义为static
//就可以解决,但static后就变成了全局的,相当于每个线程调的都是这一个入口函数,也就达不到目的了
//将this通过参数传进来就可以解决问题
static void* th_entry(void* arg)
{
threadpool*pb = (threadpool*)arg;
while(1)
{
ThreadTask task;
pb->_queue.pop(&task); //获取到节点
task.Run();
}
return NULL;
}
public:
threadpool(int maxt = MAX_THREAD,int maxq = MAX_QUEUE):_max_thread(maxt),_queue(maxq)
{
int ret;
pthread_t tid[];
for(int i = 0; i<_max_thread; ++i)
{
ret = pthread_create(&tid[i],NULL,th_entry,this);
if(ret != 0)
{
exit(-1); //构造函数没法判断成功与否,出问题就直接退出进程
}
pthread_detach(tid[i]);
}
}
bool TaskPush(const ThreadTask& task)
{
return _queue.push(task);
}
};
线程池类设计注意点:
需要注意的是,线程的入口函数,在C语言中明确定义了格式,为void* entry(void*),必须是一个void类型的参数和void返回值,这是不能改变的,在类中创建线程,入口函数也要写在类中,但问题就来了。
类中的成员方法自带this指针,而这样函数参数多了一个,系统就检测不到这是入口函数,报错:线程入口函数未定义。想要解决,就需要在类外定义,这样未免太过麻烦。直接加上static好了,这样就不带this指针了。
但是问题又来了,我们知道,通过this指针才能确保线程调用自己对应的入口函数,如果没了this指针,而且,static修饰方法,这样一来所有的线程都掉用的是同一个entry了。我们如何确保线程调用的是自己的入口函数呢?
解决办法就是,将this指针作为参数在线程创建的时候传入入口函数,这样问题都解决了。
解决方法:static修饰入口函数 + this作参数