前言
线程:线程包含了表示进程内执行环境必需的信息,其中包括进程中标识线程的线程ID、信号屏蔽字、errno变量以及线程私有数据。进程的所有信息对该进程的所有线程都是共享的。
一、线程的创建
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg)
参数说明
--thread 线程id的地址
--attr 设置线程的属性,默认为NULL
--start_routine 创建的线程函数,参数为空
--arg 把想要给线程函数传的参数放入到一个结构中,这个结构的地址就是arg,没没有要传的就为NULL
返回值
成功返回0,失败返回errno
Note:线程创建时并不能保证哪个线程会先运行:是新创建的线程还是调用线程。
二、线程的终止
如果进程中的某一线程调用了exit函数,则整个进程都会终止。
所以要想单个线程退出,就需要用到专门的函数
#include <pthread.h>
int pthread_exit(void *retval);
线程函数在结束时最好调用该函数,以保证安全干净的退出,执行完该函数后不会返回调用者
retval与上面所讲的arg类似,通过该参数向线程的回收者传递其退出信息
int pthread_join(pthread_t thread, void **retval);
进程中的其他线程可以通过调用pthread_join来获取线程号为thread的线程的退出状态
调用的线程将会一直阻塞,直到指定的线程调用pthread_exit或者从启动例程返回
int pthread_cancel(pthread_t thread);
可以调用此函数来请求取消同一进程中的其他线程
int pthread_detach(pthread_t thread);
/*分离线程*/
以上函数除了pthread_exit无返回值,其余都是成功返回0,失败返回errno
这里讲一下pthread_join和pthread_detach:
pthread_join:如果一个线程结束运行但没有被join,那么这个线程就相当于僵尸线程,调用者在join期间是阻塞的。
pthread_detach:分离一个线程,也就是把这个线程丢掉,调用者还有自己的事要做,不想等待着给你收尸,而当这个线程运行完后会自动释放资源。
下图是线程函数和进程函数之间的相似之处。
三、线程同步
当多个线程同时访问系统上的某个资源时,就需要考虑线程同步的问题,来确保任意时刻只有一个线程对资源的独占式访问。
1、互斥量
#include <pthread.h>
/*初始化互斥锁的两种方式*/
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
/*成功返回0 失败返回errno*/
互斥量(mutex) 从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。
对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住,只能回去再次等待它重新变为可用。在这种方式下,每次只有一个线程可以向前执行。
2、条件变量
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
pthread_cond_t cond=PTHREAD_COND_INITIALIZER
//初始化条件变量,attr是属性,一般置NULL
int pthread_cond_destroy(pthread_cond_t *cond);
//销毁条件变量
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
//阻塞等待一个条件变量,等待条件变量被唤醒并解锁互斥量(原子操作),返回时互斥量将再次被上锁
int pthread_cond_signal(pthread_cond_t *cond);
//唤醒至少一个等待(阻塞在pthread_wait()上的线程)
int pthread_cond_broadcast(pthread_cond_t *cond);
//唤醒全部阻塞在条件变量上的线程
/*成功返回0 失败返回errno*/
条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量。
举个例子(伪码):生产者–消费模型
必须在公共空间内由生产者先生产 消费者才能消费
void* productor(void*);//生产者线程
void* consumer(void*);//消费者线程
int main()
{
//创建两个线程
pthread_create(&pid1,NULL,productor,NULL);
pthread_create(&pid2,NULL,consumer,NULL);
pthread_join(pid1,NULL);
pthread_join(pid2,NULL);
}
void* productor(void *arg)
{
...
pthread_mutex_lock(&mutx);
/*生产中*//*注意这是公共空间,所以要加锁*/
pthread_mutex_unlock(&mutx);
/*生产完通知*/
pthread_cond_signal(&cond);
...
return NULL;
}
void* consumer(void *arg)
{
...
pthread_mutex_lock(&mutx);
while(/*发现公共空间没有东西能消费,那么我就等待*/)
pthread_cond_wait(&cond,&mutx);/*等待必须要解锁*/
/*返回后必须立马加锁*/
/*消费中*/
pthread_mutex_unlock(&mutx);
....
return NULL;
}
3、线程和进程
先思考一个问题,当多线程中的某个线程在调用fork(以下称该线程为fork线程)之前,通过pthread_create创建了一个线程(以下称该线程为兄弟线程),那么调用新创建的子进程是否会自动创建兄弟线程呢?
答案是否定的。子进程只有一个线程,那就是fork线程的完整复制,子进程会继承该父进程的互斥状态(条件变量也是一样的)。如果父进程是在fork线程中对互斥锁进行上锁解锁,子进程是能够知道的。但如果是在兄弟线程中对互斥锁执行操作,这会引发一个问题:子进程无法得知从父进程继承过来的锁的状态。
#include <pthread.h>
#include <iostream>
using namespace std;
void* brother(void *);//声明兄弟线程
int main()
{
pthread_mutex_t mutex;
pthread_t pid;
pthread_mutex_init(&mutex,NULL);
pthread_create(&pid,NULL,brother,NULL);
sleep(1);/*睡一秒是为了让fork线程有足够时间去执行兄弟线程*/
int fid=fork();
if(fid<0)
{
pthread_join(pid,NULL);//等待回收兄弟线程
pthread_mutex_destroy(&mutex);
exit(1);
}
else if(fid==0)
{
cout<<"I am child"<<endl;
/*不出意外的话子进程会一直阻塞在这里*/
pthread_mutex_lock(&mutex);
cout<<"I can't run here"<<endl;
pthread_mutex_unlock(&mutex);
exit(0);
}
else
{
cout<<"I am father"<<endl;
}
pthread_join(pid,NULL);//等待回收兄弟线程
pthread_mutex_destroy(&mutex);
exit(0);
}
void * brother(void *arg)
{
pthread_mutex_lock(&mutex);
sleep(10);
pthread_mutex_unlock(&mutex);
}
所以为了规避这样的问题发送,pthread提供了一个函数
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
/*
prepare在fork出子进程之前被执行,用来锁住父进程的所有锁;
parent是在fork子进程后,在fork返回前,在父进程中被执行,用来释放所有被prepare锁住的锁;
child是在fork返回前在子进程中被执行,作用和parent一样;
*/
void prepare()
{
pthread_mutex_lock(&mutex);
}
void parent()
{
pthread_mutex_unlock(&mutex);
}
void child()
{
pthread_mutex_unlock(&mutex);
}
该函数在fork调用前加入就行。
4、线程和信号
每个线程都能通过调用sigpromask来独立的设置信号屏蔽字,但是在多线程情况下应该使用pthread_sigmask来设置,因为sigpromask只适用于单进程单线程;
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
int sigwait(const sigset_t *set, int *sig);
四、实现一个线程池
参考https://subingwen.cn/linux/threadpool/
线程池的组成分为三个部分
1、任务队列,存储需要处理的任务,由工作的线程来处理这些任务。在C++中可使用STL的容器,在C中可以使用数组或链表来实现。
2、工作线程 N个
从任务队列中读取任务进行处理,如果任务队列为空,则该线程会被阻塞,有新任务到来(使用条件变量/信号量)解除阻塞,继续工作。
3、管理者线程 1个
管理的对象是任务队列的任务和工作线程。
(当任务过多时,创建工作线程。)
(当任务过少时,销毁工作线程。)
接下来展示伪码
首先讲一下整体框架
//main.c
#include <...>
void task_func(void*);
int main()
{
//创建线程池(允许存在的最小线程数,最大线程数,任务数)
ThreadPool *pool=PthreadPoolCreate(Min_PthreadNum, Max_PthreadNum,TaskNum);
for(int i=0;i<TaskNum;i++)
{
//把任务加到池子里(执行任务的函数和任务的参数)
AddTaskToPool(pool,task_func,task_arg);
}
//销毁池子
PthreadPoolDestroy(pool);
}
//要做的具体任务放在任务函数里
void task_func(void *arg){}
线程池的基本属性
struct pthread_pool
{
Task* taskQ; //任务队列
/*补一段任务队列的结构体
struct Task
{
//任务的函数和参数
void(*function)(void *arg);
void *arg;
}Task;
*/
int queueCapacity; // 容量
int queueSize; // 当前任务个数
int queueFront; // 队头 -> 取数据
int queueTail; // 队尾 -> 放数据
pthread_t managerID; // 管理者线程ID
pthread_t *threadIDs; // 工作的线程ID用数组存放
int minNum; // 最小线程数量
int maxNum; // 最大线程数量
int busyNum; // 忙的线程的个数
int liveNum; // 存活的线程的个数
int exitNum; // 要销毁的线程个数
pthread_mutex_t mutexPool; // 锁整个的线程池
pthread_mutex_t mutexBusy; // 锁busyNum变量
pthread_cond_t IsFull; // 任务队列是否满了,满了就阻塞主线程,不让主线程添加任务
pthread_cond_t IsEmpty; // 任务队列是否空了,空了就阻塞worker,等待主线程添加任务
int shutdown; // 是不是要销毁线程池, 销毁为1, 不销毁为0
};
接下来就是线程池的创建了
主要就是初始化各种属性,以及创建线程
ThreadPool* PthreadPoolCreate(int Min_PthreadNum, int Max_PthreadNum,int TaskNum)
{
ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));
do
{
if(pool==NULL) break;
pool->threadIDs = (pthread_t*)malloc(sizeof(pthread_t) * Max_PthreadNum);
if (pool->threadIDs == NULL) break;
memset(pool->threadIDs, 0, sizeof(pthread_t) * Max_PthreadNum);
pool->minNum = Min_PthreadNum;
pool->maxNum = Max_PthreadNum;
pool->busyNum = 0;
pool->liveNum = Min_PthreadNum; // 和最小个数相等
pool->exitNum = 0;
//初始化互斥量与条件变量,初始化成功返回值为0
if (pthread_mutex_init(&pool->mutexPool, NULL) != 0 ||
pthread_mutex_init(&pool->mutexBusy, NULL) != 0 ||
pthread_cond_init(&pool->notEmpty, NULL) != 0 ||
pthread_cond_init(&pool->notFull, NULL) != 0)
break;
//初始化任务队列
pool->taskQ = (Task*)malloc(sizeof(Task) * TaskNum);
pool->queueCapacity = TaskNum;
pool->queueSize = 0;
pool->queueFront = 0;
pool->queueRear = 0;
//1为销毁,0不销毁
pool->shutdown = 0;
// 创建线程
//1、创建一个管理线程
pthread_create(&pool->managerID, NULL, manager, pool);
//2、创建工作线程
for (int i = 0; i < min; ++i)
{
pthread_create(&pool->threadIDs[i], NULL, worker, pool);
}
//创建完毕直接返回
return pool;
}while(0);
//用do while循环一次的意义不是为了循环,而是在于当在堆上开辟空间失败时,不会直接return
//通过break跳出循环来处理创建失败的情况,这样的代码质量高
//处理创建失败的情况
if (pool && pool->threadIDs) free(pool->threadIDs);
if (pool && pool->taskQ) free(pool->taskQ);
if (pool) free(pool);
return NULL;
}
线程函数的实现
//目的:当任务过多时,创建工作线程。 当任务过少时,销毁工作线程。
void* manager(void* arg)
{
//接收来自pthread_create的参数,传的是pool池子
ThreadPool* pool = (ThreadPool*)arg;
/*1为销毁,0为不销毁*/
while (pool->shutdown==0)
{
// 获取池子中的任务数量和当前的线程数量
pthread_mutex_lock(&pool->mutexPool);
int queueSize = pool->queueSize;//任务数
int liveNum = pool->liveNum;//线程数
pthread_mutex_unlock(&pool->mutexPool);
// 取出忙的线程的数量,给busyNum单独添加互斥量是因为busyNum使用频繁
pthread_mutex_lock(&pool->mutexBusy);
int busyNum = pool->busyNum;
pthread_mutex_unlock(&pool->mutexBusy);
// 添加线程
if (/*可以自己指定条件添加线程*/)
{
pthread_mutex_lock(&pool->mutexPool);
for (/*遍历存放工作id的线程数组*/)
{
if (pool->threadIDs[i] == 0)//表示当前位置还没有被创建线程(前面我们将其memset全部初始化为0了)
{
pthread_create(&pool->threadIDs[i], NULL, worker, pool);
pool->liveNum++;
}
}
pthread_mutex_unlock(&pool->mutexPool);
}
// 销毁线程
if (/*可以自己指定条件销毁线程*/)
{
// 让工作的线程自杀
for (...)
{
//销毁的工作让worker去做
pthread_cond_signal(&pool->IsEmpty);
}
}
}
return NULL;
}
看woker线程函数如何工作
//工作线程函数
void* worker(void* arg)
{
//1、接收arg参数
ThreadPool* pool = (ThreadPool*)arg;
//2、对任务队列循环检测是否有任务
while (1)
{
pthread_mutex_lock(&pool->mutexPool);
// 当前任务队列为空且不销毁池子
while (pool->queueSize == 0 && pool->shutdown==0)
{
// 阻塞等待直到池子不为空(通过pthread_cond_singal的通知过来)
pthread_cond_wait(&pool->IsEmpty, &pool->mutexPool);
// 判断是不是要销毁线程,刚刚在manger线程中提到过让worker来销毁
if (pool->exitNum > 0)
{
pool->exitNum--;
//一定要存活的线程大于允许存在的最小线程
if (pool->liveNum > pool->minNum)
{
pool->liveNum--;
pthread_mutex_unlock(&pool->mutexPool);
threadExit(pool);//销毁线程
}
}
}
// 如果要销毁线程池
if (pool->shutdown)
{
pthread_mutex_unlock(&pool->mutexPool);
threadExit(pool);
}
// 开始做任务了,从任务队头中取出一个任务
Task task;
task.function = pool->taskQ[pool->queueFront].function;
task.arg = pool->taskQ[pool->queueFront].arg;
// 移动头结点,使头节点在0~queueCapacity之间循环
pool->queueFront = (pool->queueFront + 1) % pool->queueCapacity;
//解决完一个任务就能把一个任务丢了
pool->queueSize--;
// 解锁
pthread_cond_signal(&pool->notFull);
pthread_mutex_unlock(&pool->mutexPool);
pthread_mutex_lock(&pool->mutexBusy);
pool->busyNum++;
pthread_mutex_unlock(&pool->mutexBusy);
//执行任务
task.function(task.arg);
free(task.arg);//这个参数在main线程中malloc出来的,所以要记得执行完后free
task.arg = NULL;
pthread_mutex_lock(&pool->mutexBusy);
pool->busyNum--;
pthread_mutex_unlock(&pool->mutexBusy);
}
return NULL;
}
接下来看如何实现添加任务AddTaskToPool
void AddTaskToPool(ThreadPool* pool, void(*func)(void*), void* arg)
{
//往池子里添加任务当然要锁池子
pthread_mutex_lock(&pool->mutexPool);
//池子满了且暂不销毁池子
while (pool->queueSize == pool->queueCapacity && !pool->shutdown)
{
// 池子满当然会阻塞(main线程阻塞),等待pthread_cond_signal传来&pool->IsFull的信息
pthread_cond_wait(&pool->IsFull, &pool->mutexPool);
}
//如果要销毁池子,就解锁池子并返回。
if (pool->shutdown)
{
pthread_mutex_unlock(&pool->mutexPool);
return;
}
// 添加任务,队尾添加,并让任务标加一
pool->taskQ[pool->queueRear].function = func;
pool->taskQ[pool->queueRear].arg = arg;
//这里是在让队列的下标在0~queueCapacity之间循环
pool->queueRear = (pool->queueRear + 1) % pool->queueCapacity;
pool->queueSize++;
//添加完任务通知worker池子里有东西了,并解锁池子
pthread_cond_signal(&pool->IsEmpty);
pthread_mutex_unlock(&pool->mutexPool);
}
最后就是销毁整个线程池了PthreadPoolDestroy
在这之前看一下如何销毁单个线程threadExit
void threadExit(ThreadPool* pool)
{
pthread_t tid = pthread_self();
for (int i = 0; i < pool->maxNum; ++i)
{
if (pool->threadIDs[i] == tid)
{
pool->threadIDs[i] = 0;
break;
}
}
pthread_exit(NULL);
}
int PthreadPoolDestroy(ThreadPool* pool)
{
if (pool == NULL)
{
return -1;
}
// 关闭线程池
pool->shutdown = 1;
// 阻塞回收管理者线程
pthread_join(pool->managerID, NULL);
// 唤醒阻塞的消费者线程,还是让worker线程去销毁
for (int i = 0; i < pool->liveNum; ++i)
{
pthread_cond_signal(&pool->IsEmpty);
}
// 释放堆内存
if (pool->taskQ)
{
free(pool->taskQ);
}
if (pool->threadIDs)
{
free(pool->threadIDs);
}
/*销毁互斥量,条件变量*/
pthread_mutex_destroy(..);
pthread_cond_destroy(..);
free(pool);
pool = NULL;
return 0;
}
以上就是一个简单线程池的基本框架。