一、传统的并发处理
并发基本概念:所谓的并发编程是指在同一台计算机上“同时”处理多个任务。并发是在同一个实体上的多个事件。
处理事件“阻塞”怎么办?
①忙于漫长的CPU密集型处理②读取文件,但文件尚未缓存,从硬盘中读取较为缓慢③不得不获取某个资源:硬件驱动、互斥锁、等待同步方式调用的数据库响应、网络上的请求和响应。
单个进程或线程同时只能处理一个任务,如果有很多请求需要同时处理怎么办?
解决方案:运用多进程或多线程技术解决
缺陷:
①创建和销毁线程上花费的时间和消耗的系统资源,甚至可能要比花在实际的用户请求的时间和资源多得多;
②活动的线程需要消耗系统资源,如果启动太多,会导致系统由于过度消耗内存或“切换过度”而导致系统资源不足。
解决方案→线程池
二、引入线程池
线程池:由一个任务队列和一组处理队列的线程组成。一旦工作进程需要处理某个可能“阻塞”操作,不用自己操作,将其作为一个任务放到线程池的队列,接着会被某个空闲线程提取处理。
线程池实现的核心组件:
任务:待处理的工作,通常由标识、上下文和处理函数组成
任务队列:按顺序保存待处理的任务队列,等待线程中的线程组处理
线程池:由多个已启动的一组线程组成
条件变量:一种同步机制,允许线程挂起,直到共享数据上的某些条件得到满足
互斥锁:保证在任意时刻,只能有一个线程访问该对象
线程池的好处
-
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
-
提高线程的可管理性。
三、线程池实现剖析
任务队列实现:主要包括两个类Task和TaskQueue.
注意:添加和取出操作是原子操作,需要加锁和解锁。
互斥锁需要在任务队列的构造函数初始化,析构函数中销毁
任务Task实际上是封装了要执行的函数和他的参数,TaskQueue类实际上是封装了一个储存任务的队列,并用锁实现原子操作
//TaskQueue.h
#pragma once
#include<queue>
#include<pthread.h>
using namespace std;
using callback= void*(*)(void*);
struct Task{
Task():function(nullptr),arg(nullptr){}
Task(callback f,void * targ):function(f),arg(targ) {}
callback function;
void* arg;
};
class TaskQueue{
public:
TaskQueue();
~TaskQueue();
void addTask(Task &task);
Task takeTask();
size_t size();
private:
pthread_mutex_t m_mutex;
queue<Task> m_queue;
};
TaskQueue.cpp
#include"TaskQueue.h"
#include<iostream>
using std::cout;
using std::endl;
TaskQueue::TaskQueue(){
pthread_mutex_init(&m_mutex,NULL);
}
TaskQueue::~TaskQueue(){
pthread_mutex_destroy(&m_mutex);
}
void TaskQueue::addTask(Task &task){
pthread_mutex_lock(&m_mutex);
m_queue.push(task);
pthread_mutex_unlock(&m_mutex);
cout<<"add task, current task number is "<<m_queue.size()<<endl;
}
Task TaskQueue::takeTask(){
pthread_mutex_lock(&m_mutex);
Task task;
if(m_queue.size()!=0){
task=m_queue.front();
m_queue.pop();
}
pthread_mutex_unlock(&m_mutex);
cout<<"take task, current task number is "<<m_queue.size()<<endl;
return task;
}
size_t TaskQueue::size(){
return m_queue.size();
}
3.1互斥量概念和使用:
互斥量可以使用pthread的互斥接口来保护数据,确保同一时间只有一个线程访问数据。互斥量可以从本质上说是一把锁,在访问数据前对互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量;加锁以后任何其他视图再次对互斥量加锁的线程都会被阻塞,直到当前线程释放该互斥锁;如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对互斥量加锁,其他线程就会看到互斥量依然是锁着的,只能再次等待它重新变为可用(在这种方式下,每次只有一个线程可以向前执行)
互斥变量数据类型:pthread_mutex_t
互斥量属性结构体:pthread_mutexattr_t
互斥量属性(3个):
进程共享属性(pshared)、健壮属性(robust)、类型属性(type)
互斥量属性的创建和销毁
#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
进程共享属性是指:允许相互独立的多个进程把同一个内存数据块映射到它们各自独立的地址空间中。就像多个线程访问共享数据一样,多个线程访问共享数据也需要同步(互斥)
3.2互斥变量的初始化和释放
1.静态初始化
直接把pthread_mutex_t互斥变量设置为常量PTHREAD_MUTEX_INITIALIZER
静态初始化互斥变量只能拥有默认的互斥量属性,不能设置其他互斥量属性
例如:
pthread_mutex_t mutex;
mutex=PTHREAD_MUTEX_INITIALIZER;
//或者
pthread_mutex_t *mutex=(pthread_mutex_t *)malloc(sizeof(pthread_mutex_t));
*mutex=PTHREAD_MUTEX_INITIALIZER;
2.动态初始化
静态初始化互斥变量只能拥有默认互斥量属性,我们可以通过pthread_mutex_init函数来动态初始化互斥量,并且可以在初始化时选择设置互斥量的属性
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 返回值:成功返回0,否则返回错误编号
pthread_mutex_init:
1.
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,NULL);
/*do something*/
pthread_mutex_destroy(&mutex);
2.
pthread_mutex_t* mutex=(pthread_mutex_t*)malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(mutex,NULL);
/*do something*/
pthread_mutex_destroy(mutex);
free(mutex);
3.互斥量的加锁和解锁
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//对互斥量进行尝试加锁(非阻塞)。如果互斥量处于未加锁状态,那么pthead_mutex_trylock就会锁住这个互斥量;如果此锁处于加锁状态,那么pthead_mutex_trylock就出错返回EBUSY,并且不会阻塞
// 返回值:成功返回0,否则返回错误编号
4.超时互斥锁(pthread_mutex_timedlock)
当当线程试图获取一个已加锁的互斥量时,pthread_mutex_timedlock允许绑定线程阻塞时间
在达到超时时间值时,如果还不能对互斥量成功加锁,那么就返回错误码ETIMEDOUT
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict tsptr);