一、预备知识
1、线程:
线程同进程类似,是允许应用程序并发执行多个任务的一种机制,一个进程可以包含多个线程。在Linux下,进程是系统资源的最小分配单位,线程则是最小的执行单位,一个进程中至少有一个线程作为程序执行的主线程。因为进程作为系统资源的最小分配单位,而线程在进程的基础上再对分配给进程的系统资源进行再次分配,因此同一进程的线程间可以通过全局变量进行通信;而进程间通信则需要系统提供的一些IPC机制来完成。Linux 开始并不存在线程,线程在Linux下的实现类似于进程,对于Linux内核不论线程还是进程都是以进程的方式运行的。Linux下并不会提供线程的API函数,因此需要手动下载安装线程库,以下是Ubuntu安装方式:
sudo apt-get install glibc-doc
sudo apt-get install manpages-posix-dev
2、linux 下线程API相关
①、数据类型
pthread_t:————————————->线程ID
pthread_mutex_t:———————->互斥对象
pthread_mutexattr_t:—————->互斥属性对象
pthread_cond_t:———————->条件变量
pthread_condattr_t:——————>条件变量的属性对象
pthread_key_t:————————>线程特有数据的键
pthread_once_t:———————–>一次性初始化控制上下文
pthread_attr_t:————————>线程的属性对象
②API
//线程的创建
int pthread_create(pthread_t *thread——–>线程创建之后返回的线程ID,之后的线程相关函数可以使用该id来引用此线程
const pthread_attr_t *attr————————>指定了新线程的各种属性,一般传NULL
void*(start)(void ), void *arg———————>线程创建成功之后自动执行的回调函数
);
//线程的终止
void pthread_exit( void *reval————>线程的返回值,该值不可放在线程栈中(造成浅拷贝)
);
线程的终止方法:
1>在线程的回调函数中执行return
2>线程调用pthread_exit(),pthread_exit()函数会终止调用线程,其返回值可以由另一线程调用pthread_join()来获取。
3>调用pthread_cancel()
4>任意线程调用exit(),或者主线程调用了return都会导致所有线程立即终止。
注意:主线程(主函数中)调用pthread_exit(),其它线程将继续运行。
//获取线程自身ID
pthread_t pthread_self(void);
//检查两个线程id是否相同;
int pthread_equal(pthread_t t1, pthread_t t2);
//连接已终止的线程(等待thread线程终止,若线程已经终止,则立即返回)
int pthread_join(pthread_t thread,—————>等待要终止的线程
void **retval——————————————–>线程终止时返回值
);
注意:之前连接过的线程,一定不要再次连接。终止的线程一定要使用该函数进行连接,否则会产生僵尸线程。该函数只能阻塞连接,可以使用条件变量实现类似非阻塞连接。
//线程分离
int pthread_detach(pthread_t thread);
该函数将指定线程标记为分离状态。处于分离状态的线程不再受到pthread_join()函数影响,但主函数调用return退出,或者其它线程调用exit()退出,即处于分离状态的线程对应进程退出时它才退出。
二、线程池的实现方法
线程池的实现类似于生产者消费者模型。
实现线程池首先需要定义一个线程池的结构体来控制线程的运行和终止;其次,要满足不同线程执行不同的任务,而且在程序执行过程中还可以动态加入任务的功能,还需要定义一个任务链表。
具体实现如下:
typedef void*( *Start_Work )( void *args );//线程创建后要调用的回调函数类型,这个是任务队列添加任务时需要传入的回调函数类型。为了书写方便,取了别名
typedef struct ThreadWorker //任务列表
{
Start_Work doWork; //回调函数
void *args; //回调函数的参数
struct ThreadWorker *next;
}ThreadWorker;
typedef struct ThreadPool //线程池结构体
{
int maxThreadNum; //线程池大小
int curWorkLine; //
int shutDown; //线程池是否结束
pthread_mutex_t lock; //互斥锁
pthread_cond_t ready; //是否有任务
ThreadWorker *head; //任务队列
pthread_t *threadId; //池中线程ID
}ThreadPool;
线程池函数接口声明
int ThreadPool_Init(int num); //初始化线程池
void *Thread_Routine( void *args ); //每个线程创建后都执行的回调,这是线程池工作的主阵地。
int ThreadPool_AddWork (Start_Work doWork, void *args); //向线程池的任务队列加入任务。
int ThreadPool_Destroy(); //摧毁线程池。
线程池函数接口的实现
#include "ThreadPool_op.h"
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
static ThreadPool *pool = NULL; //线程池句柄,全局的不希望上层操作就可以这样定义
//线程池的初始化
int ThreadPool_Init( int num )
{
int ret = 0; //返回值检测,养成好习惯。
pool = ( ThreadPool * ) malloc ( sizeof ( ThreadPool ) ); //创建一个线程池
if( pool == NULL )
{
printf ( "pool Init error\n" );
return -1;
}
memset ( pool, 0, sizeof (ThreadPool) );//线程池内存置零
pthread_mutex_init ( &( pool ->lock ), NULL );//互斥锁初始化
pthread_cond_init ( &( pool ->ready ), NULL );//条件变量初始化
pool ->maxThreadNum = num;
pool ->curWorkLine = 0;
pool ->shutDown = 0; //线程池不退出
ret = (pool ->threadId) = ( pthread_t * ) malloc ( sizeof ( pthread_t ) * (pool ->maxThreadNum) ); //分配num个线程id内存用来存放线程ID
if( ret == NULL )
{
printf ( "pool ->threadId error\n" );
return -1;
}
// get "num" thread id;
for( int i = 0; i < (pool ->maxThreadNum); ++i )//循环创建num个线程
{
ret = pthread_create ( &(pool ->threadId[ i ]), NULL, Thread_Routine, NULL ); //注意传入的回调函数。主阵地
if( ret != 0 )
{
return -1;
}
}
return 0;
}
//线程池运行主阵地
void *Thread_Routine(void *args)
{
while(1) //线程执行完默认退出,不能让线程池中的线程退出,它们需要循环的执行任务。
{
//printf ( " Thread %u create successful\n ", pthread_self () );//打印一句废话,调试时用到
pthread_mutex_lock ( &( pool ->lock ) ); //上锁 (num 个线程创建成功后,只有一个线程可以向下执行,其它num-1个线程阻塞在这里,等待那个线程解锁)
while ( ((pool ->shutDown) == 0) && \
(pool ->curWorkLine) == 0 )//如果线程池不退出并且任务队列为空
{
printf ( " Thread %u is waiting for working\n ", pthread_self () );//
pthread_cond_wait ( &( pool ->ready ), &( pool ->lock ) ); //刚刚进来的那一个线程会因为任务队列没有任务而在这等待,只有当新任务加入之后,它才会继续向下执行
}
if( ( pool ->shutDown ) == 1 ) //线程池开始摧毁
{
pthread_mutex_unlock ( &( pool ->lock ) );//解锁互斥锁
pthread_exit( NULL ); //线程池中所有线程退出的的地方。
}
(pool ->curWorkLine) --; //进来的那个线程开始干活,任务队列中任务减一
ThreadWorker *worker = (pool ->head); //得到任务链表中的第一个任务
pool ->head = (pool ->head) ->next; //任务链表中去掉第一个任务,直行第二个任务(没有第二个任务自然指向NULL)
pthread_mutex_unlock ( &(pool ->lock) );//获得任务之后解锁,另一个线程进来开始在上述条件变量处等待新的任务。注意,加锁的区间越小越好,任务队列是公共资源,只对任务队列加锁就可以了,解决了线程的冲突问题,还提高并发。
(*( worker ->doWork )) ( worker ->args );//开始执行获得的任务
free ( worker ); //执行完任务之后就释放刚刚获得的任务列表节点
worker = NULL; //好习惯要置空
} //线程回到开始,等待任务
}
//向任务队列中加入任务
int ThreadPool_AddWork(Start_Work doWork, void *args)//第一个参数就是线程要执行的回调类型,我在上文取别名了,第二个参数是任务的执行参数,如果需要传入多个参数,可以写个结构体,当然上述线程执行的函数也要稍微修改一下才可以
{
//申请任务节点并做值的初始化。为了减少任务队列上锁时间,提高并发所以先初始化,再把任务挂到任务列表中。
ThreadWorker *pCur = NULL;
ThreadWorker *pTmp = ( ThreadWorker * ) malloc ( sizeof ( ThreadWorker ) );
memset ( pTmp, 0, sizeof (ThreadWorker) );
pTmp ->doWork = doWork;
pTmp ->args = args;
pTmp ->next = NULL;
//挂任务之前要上锁任务列表防止别的线程取任务而造成冲突。
pthread_mutex_lock ( &(pool ->lock) );
//在任务列表头部取任务,在尾部插入新的任务。
pCur = pool ->head;
while ( 1 )
{
if( (pool ->head) == NULL )
{
pool ->head = pTmp;
break;
}
else if ( pCur ->next == NULL )
{
pCur ->next = pTmp;
break;
}
pCur = pCur ->next;
}
//任务挂到任务队列中,之后任务个数加1
( pool ->curWorkLine ) ++;
//解锁任务队列,允许取任务
pthread_mutex_unlock ( &(pool ->lock) );
//发信号,通知阻塞在条件变量上的那个线程,任务队列中加入新的任务了,可以开始工作了。这里要多悟,否则理解不彻底。
pthread_cond_signal ( &(pool ->ready) );
return 0;
}
//线程池的摧毁
int ThreadPool_Destroy()
{
if( (pool ->shutDown) == 1 ) //防止二次摧毁造成错误
{
return -1;
}
pool ->shutDown = 1; //将标志位置1表示开始摧毁线程池
pthread_cond_broadcast ( &(pool ->ready) );//欺骗所有阻塞在条件变量上的线程,让它们自己退出
for( int i = 0; i < (pool ->maxThreadNum); ++i ) //连接所有线程,防止僵尸线程
{
printf ( "The thread: %u is exiting\n", pthread_self () );
pthread_join ( (pool ->threadId[ i ]), NULL );
}
if( (pool ->threadId) != NULL ) //释放存储线程ID空间的资源
{
free ( pool ->threadId );
}
ThreadWorker *pHead = NULL;
ThreadWorker *pNext = NULL;
pHead = (pool ->head);
while( pHead != NULL ) //从头到尾的释放任务队列
{
pNext = pHead ->next;
free( pHead );
pHead = pNext;
pHead = pHead ->next;
}
pthread_mutex_destroy ( &(pool ->lock) );//摧毁互斥锁
pthread_cond_destroy ( &(pool ->ready) );//摧毁条件变量
if( pool != NULL ) //释放线程池占用内存
{
free ( pool );
}
return 0;
}
以上便是线程池的所有实现代码。将任务加入任务队列是不是比较像生产者,线程池是不是比较像消费者?这就是一个典型的生产者消费者模型
4附上我的测试代码
#include <stdio.h>
#include <unistd.h>
#include "ThreadPool_op.h"
static int count = 0; //从0开始数数
pthread_mutex_t myTest; //这个互斥锁是我测试线程数数时防止冲突写的,与线程池无关。
void* My_Work( void *args ); //数数的回调,把它加入任务队列中
int main(void)
{
int ret = 0;
int num = 20; //线程池初始化 20 个线程
pthread_mutex_init ( &myTest, NULL );
ret = ThreadPool_Init ( num );
if( ret < 0 )
{
printf( "Thread init error \n" );
ThreadPool_Destroy ();
}
//给线程池的任务队列加任务
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
ThreadPool_AddWork( My_Work, NULL );
sleep( 10000 ); //防止主线程退出,主线程return,其他线程都会退出,当然可以尝试pthread_exit()结束主线程,其它线程不会退出,但是要另外起一个线程做资源回收或者线程池的摧毁等任务,可以用条件变量去实现。试试...
ThreadPool_Destroy ();
pthread_mutex_destroy ( &(myTest) );//摧毁数数的互斥锁
return 0;
}
void* My_Work( void *args )
{
pthread_mutex_lock( &myTest );
printf( " The thread %u \tcount: %d \n", pthread_self (), ++count );
pthread_mutex_unlock ( &myTest );
}
三、总结:
1、同一进程中线程共享全局、堆变量、进程ID、打开的文件描述符、信号处置、当前工作目录,线程在使用公有变量的时候要注意解决冲突。
2、线程池的本质就是典型的生产者消费者模型,线程池的主要两个动作就是:1>将任务挂到线程池的任务队列中去;2>执行时将任务从任务队列中取出来。要解决好对任务队列添加和取节点的冲突问题就可以了。
3、最后还是附上我练习线程池时的代码方便初学者学习,我的代码是在Linux下QT编写。
https://github.com/dingjingmaster/ThreadPool_Test.git