目录
多线程
线程(thread)理论
进程和线程的区别(面试重点)
线程的优势(面试重点)
多线程编程pthread
线程的创建pthread_create()
主线程等待子线程可以用pthread_join()
线程退出pthread_exit()
取消线程pthread_cancel()
设置线程取消属性pthread_setcancelstate()
多线程案例
线程同步常用机制
互斥锁mutex
条件变量Conditions
线程池(面试重点)
创建线程池create_thread_pool()
初始化线程池
线程处理函数(负责睡眠和获取任务)
添加任务thread_pool_add()
唤醒线程池中的线程
取任务并执行
重新进入睡眠
销毁线程池thread_pool_destroy()
上节我们学习了进程间的通信,本节开始学习多线程编程!
多线程
前面说了我们的电脑可以支持我们同时进程多个进程,用QQ聊天,这是一个进程,然后打开浏览器又是一个进程,但是如果在QQ这一个进程里要同时跟很多个人聊天,那是不是每跟一个人聊天就又打开一个进程呢?如果你的QQ好友有一千个,那你得打开一千个进程,显然我们前面学习的多进程并不是解决问题的最佳方案。
为了解决这个问题,系统又给我们加了一个概念:多线程
什么叫线程?
通俗来讲,线程就是“进程里面的进程”,叫执行流
就像这样,下图一个箭头代表一个进程,一个进程分为两个线程,一个线程可以单独处理一件事情,那一个进程里面有两个线程,也就是两个分支,这样一个进程就可以处理两件事情,而这两个线程是共享这一个进程的虚拟空间的。
在32位的系统中,启动一个进程占用4个G的虚拟地址空间(如果是64位的系统,就不只4个G)
但是启动一个线程是不需要分配地址空间的,这就是线程的好处。
在服务器端的开发中,需要并发处理时,也就是一台服务器要同时处理很多个客户端,可以用多进程来解决,也可以用多线程来解决,但是一般来讲多线程肯定要比多进程要更合适些。
线程(thread)理论
线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程.现在,多线程技术已经被许多操作系统所支持,包括Windows/NT、Linux。
进程和线程的区别(面试重点)
进程和线程的根本区别是进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮(多进程的一个优点)。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
线程的优势(面试重点)
和进程相比,它是一种非常“节俭”的多任务操作方式.,启动一个线程不需要分配内存资源。
线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
线程有方便的通信机制(比如定义了一个全局变量,每个线程都可以使用,因为它们是存在同一块地址空间里面的)。
使多CPU系统更加有效,多个线程可以同时被调度到多个CPU上(比如你的电脑是8核的,现在启动了8个线程,那这8个线程可以平均地分配到8个核上面,这就是真正的并发,不是伪并发)。
改善程序结构。
多线程编程pthread
Linux系统下的多线程遵循POSIX线程接口,称为pthread。
编写Linux下的多线程程序,需要使用头文件pthread.h,编译时需要链接多线程的库libpthrea.so。
编译的时候要加上-lpthread,比如:gcc test.c -o test -lpthread
线程的创建pthread_create()
用pthread_create()这个函数来创建线程
补充命令30:对于多线程编程,查找man手册时,直接输入man 函数名,中间不需要加1 2 3这些数字
第一个参数是线程号的地址(线程号放在一个pthread_t类型(一个长整型的整数用%ld打印)的数组里面)。
第二个参数是线程的属性,一般写NULL就行。
第三个参数*号表示start_routine是一个指针,指向一个函数,这个函数的参数的类型是void*,返回值也是void*类型,这个函数其实是线程函数,我们创建线程是为了完成一件事情,把完成一件事情的程序放在一个函数里面,我们就是把这个函数的函数名作为这第三个参数。
第四个参数就是要给start routine指向的这个函数传一个参数,我们就将这个参数的地址传过去,不传可以直接写为NULL。
这个函数的返回值就是成功返回0,失败返回错误的数字
代码演示:
补充命令31:gcc 要编译的文件名.c -o 要生成的二进制文件名 -lpthread
多线程在编译的时候需要手动链接线程库,只要代码中用了多线程就得加上 -lpthread库再编译。
运行结果:
可以看到运行后并没有什么输出
为什么?
以上这段代码有三个执行流,一个是主分支,在主分支里面有两个分支,也就是在这个主分支里面有两个线程。当主线程往下执行的时候,中间创建了两个线程,两个线程开始执行,但是主线程运行到return 0就结束了,所以另外两个分支还没有来得及运行。
因此我们保证当那两个线程在运行的时候,主线程不能提前结束,主线程结束就相当于这个进程结束了。
主线程等待子线程可以用pthread_join()
让主线程等待子线程可以用pthread_join();这个函数
第一个参数是线程号,第二个参数是线程的返回值(即线程的状态,我们需要定义一个指针,把这个指针的地址传给这个函数,在这个函数体里面,它会把线程的状态放在这个指针指向的那个地址)
我们在主线程结束前加上等待
运行后就可以看到子线程打印出来东西了
注:pthread_join这个函数是阻塞的,它会一直等到执行的子线程结束后,程序才会往下继续执行。
我们加上这两句可以看到两个进程结束的顺序
线程退出pthread_exit()
我们上面写的程序是程序执行完了线程正常退出。我们也可以加一点东西让它中途退出
我们可以用pthread_exit这个函数
它的参数是一个void*类型的指针,存放的是返回值,这个返回值我们可以自己定义。
这样线程退出的状态就是100
我们将可以看到这个100直接保存在我们定义的status这个指针里面
我们打印出来看看
还有一种退出情况是一个线程把另一个线程取消掉
取消线程pthread_cancel()
取消用pthread_cancel这个函数
参数是要取消的那个线程的线程号
这里要把数组改成全局变量,这样两个线程才都能使用,这就是线程之间通信的优势。
这样线程1结束线程2跟着也就结束了
能不能让线程2禁止其他线程来取消它呢?
可以,我们可以修改线程2的属性为不能被取消
设置线程取消属性pthread_setcancelstate()
我们要用到pthread_setcancelstate()这个函数
第一个参数可以是状态:PTHREAD_CANCEL_ENABLE 允许被取消和PTHREAD_CANCEL_DISABLE不允许被取消
第二个参数是记录原来的状态(需要自己定义一个变量,传给这个函数,它会把原来的状态存放到这个变量中)
这样它就没有被线程1取消了
多线程案例
之前我们学习多进程之间的通信的时候,只实现过一个进程收和一个进程发,但是负责收数据的那个进程也想给发数据的那个进程发东西怎么办?
接下来就用多线程来实现这样的需求
代码演示
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#define MSGKEY 1000//区分消息队列
struct msgbuf
{
long mtype; //即消息的类型
char mtext[128]; //数组的长度不够用可以改
};
pthread_t tid[2]={0};
//接收线程
void *recv_thread(void *arg)
{
int msgid=*(int*)arg;
struct msgbuf m;
while(1)
{
if(msgrcv(msgid,&m,sizeof(m.mtext),1,0)==-1)//从消息队列里面收,放到mtext里面
{
perror("msgrcv");
break;
}
//如果收到的是bye就退出
if(!strcmp(m.mtext,"bye"))
{
pthread_cancel(tid[1]);//取消发送线程再退出
break;
}
//收到数据之后就把它打印出来
printf("\t\t%s\n",m.mtext);
memset(&m,0,sizeof(m));
}
}
//发送线程
void*send_thread(void*arg)
{
int msgid=*(int*)arg;
struct msgbuf m;//创建结构体
//发送数据,不停地发送,遇到bye结束
while(1)
{
scanf("%s",m.mtext);//从键盘上获取消息
m.mtype=2;//消息类型
if(msgsnd(msgid,&m,sizeof(m.mtext),0)==-1)
{
perror("msgsnd");
break;//跳出循环
}
if(!strcmp(m.mtext,"bye"))
{
pthread_cancel(tid[0]);//取消接收线程再退出
break;//跳出循环
}
memset(&m,0,sizeof(m));//清空结构体
}
}
int main()
{
//获取消息队列
int msgid=msgget(MSGKEY,0);
if(-1==msgid)
{
perror("msgget");
exit(1);
}
//启动线程
if(pthread_create(&tid[0],NULL,recv_thread,&msgid)!=0)
{
perror("pthread_create");
exit(2);
}
if(pthread_create(&tid[1],NULL,send_thread,&msgid)!=0)
{
perror("pthread_create");
exit(3);
}
//线程等待和回收
void*status;
pthread_join(tid[0],&status);
pthread_join(tid[1],&status);
return 0;
}
运行结果
这样双方都可以发送和接收了
但是有个小瑕疵就是./2.msg-send这边输入bye的时候程序并没有退出,只有在./3.msg-recv这边输入bye的时候才全部退出
为什么呢?
因为在./2.msg-send这份代码中发送线程获取了bye之后结束了发送线程
发送线程结束后被回收了,但是这个进程中的接收线程还没有结束。
所以当我们这个进程发送bye之后,发送线程结束, 但是程序还卡在了它的接收线程这里不结束,我们要做的就是但这个进程的发送线程获取到bye之后,接收线程也要和它的发送接线一起结束,整个进程才能结束。
我们可以在发送线程跳出循环之前先把接收线程取消掉
在接收线程跳出循环之后先把发送线程取消掉
两份代码都这样修改
这样就是一遍输入bye就能同时结束两个进程了
线程同步常用机制
互斥锁mutex
之前我们说过只要涉及到多进程并发的情况,都需要考虑到进程执行的顺序。进程里面我们学习了进程同步,让进程有序地去访问临界资源,使用的机制是信号量。
同样,当我们的进程中有多个线程要同时进行时,也要让线程有序地访问共享资源,而线程同步常用的有三种机制:
1、互斥量Mutex
2、信号灯Semaphore(这个和进程中的信号量一样)
3、条件变量Conditions
互斥量我们也叫互斥锁,可以理解为就是一把锁,类似于我们之前学的信号量(信号灯),就是当一个线程进去访问共享资源时就把门给锁起来,访问完成后再把门给解开,让另一个线程进去访问。
使用互斥锁之前要定义一个互斥锁,
然后初始化互斥锁
用完之后还要销毁互斥锁
记得在访问数据之前要加锁
访问数据结束后要解锁
注意:加锁和上锁之间的代码越简洁越好,也就是说访问数据完之后要赶紧出去,因为别的线程还在那边等待着。
我们可以模拟一个卖票的场景:
比如说现在有100张,我们启动5个线程,每个线程都去卖票,每卖掉一张票,就让票数减1。
代码演示:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
//定义互斥锁
pthread_mutex_t mutex;
//延时函数
void delay()
{
int i,j;
for(i=0;i<10000;i++)
for(j=0;j<50000;j++);
}
int g_ticket=100;//总的票数
//多个线程可以共享同一个线程函数,但是执行的时间是不一样的
void* sale_ticket(void*arg)
{
int cur;//当前票数
while(1)
{
//上锁
pthread_mutex_lock(&mutex);
cur=g_ticket;
if(cur<=0)
{
//退出之前要解锁
pthread_mutex_unlock(&mutex);
break;
}
printf("%ld get %d ticket\n",pthread_self(),cur);//pthread_self()获取线程号,返回值是pthread_t长整形
cur--;
g_ticket=cur;
//解锁,解锁最好在延时函数前
pthread_mutex_unlock(&mutex);
delay();
}
}
int main()
{
//初始化信号量
pthread_mutex_init(&mutex,NULL);//互斥锁的属性,不需要写成NULL
//创建5个进程
int i;
pthread_t tid[5]={0};
for(i=0;i<5;i++)
{
if(pthread_create(&tid[i],NULL,sale_ticket,NULL)!=0)
{
perror("pthread_create");
exit(1);
}
}
//等待和收回线程
void*status;
for(i=0;i<5;i++)
{
pthread_join(tid[i],&status);
}
//销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
运行结果
......
条件变量Conditions
在线程同步中,还有一种机制,就是条件变量
顾名思义就是当满足某个条件的时候才让某个线程运行,不满足就让某个线程等待。
之后我们要学习的线程池就会用到这个条件变量。
我们还是用上面卖票的场景来测试。
现在规定就两个线程来卖票,总共100张票,前50张只能让A线程来卖,后50张才让B线程开始卖。
那50张就是临界条件。
注意:使用条件变量一定要配合互斥锁来使用
使用条件变量之前要定义一个条件变量
然后初始化条件变量
最后用完之后要销毁条件变量
在满足条件前得先让那另一个线程处于睡眠状态,醒来后再更新数据
在先执行的线程中添加条件变量,如果满足就唤醒另一个线程
代码演示
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
//定义互斥锁
pthread_mutex_t mutex;
//定义条件变量
pthread_cond_t cond;
//延时函数
void delay()
{
int i,j;
for(i=0;i<10000;i++)
for(j=0;j<50000;j++);
}
int g_ticket=100;//总的票数
//b线程
void* sale_ticket_b(void*arg)
{
int cur;//当前票数
while(1)
{
//上锁
pthread_mutex_lock(&mutex);
cur=g_ticket;
if(cur<=0)
{
//退出之前要解锁
pthread_mutex_unlock(&mutex);
break;
}
//如果票数大于50就睡眠
if(cur>50)
{
pthread_cond_wait(&cond,&mutex);//b线程拿到了条件变量,既然睡眠了就先把之前上的锁释放掉先
cur=g_ticket;//被唤醒之后获取新的票数
}
printf("%ld get %d ticket\n",pthread_self(),cur);//pthread_self()获取线程号,返回值是pthread_t长整形
cur--;
g_ticket=cur;
//解锁,解锁最好在延时函数前
pthread_mutex_unlock(&mutex);
delay();
}
}
//a线程
void* sale_ticket_a(void*arg)
{
int cur;//当前票数
while(1)
{
//上锁
pthread_mutex_lock(&mutex);
cur=g_ticket;
if(cur<=0)
{
//退出之前要解锁
pthread_mutex_unlock(&mutex);
break;
}
//如果票数等于50就唤醒b线程,群发的,但是只有拿到这个条件变量的线程才会响应
if(cur==50)
{
pthread_cond_signal(&cond);//发送信号,谁获得了条件变量谁就被唤醒
}
printf("%ld get %d ticket\n",pthread_self(),cur);//pthread_self()获取线程号,返回值是pthread_t长整形
cur--;
g_ticket=cur;
//解锁,解锁最好在延时函数前
pthread_mutex_unlock(&mutex);
delay();
}
}
int main()
{
//初始化信号量
pthread_mutex_init(&mutex,NULL);//互斥锁的属性,不需要写成NULL
//初始化条件变量
pthread_cond_init(&cond,NULL);
//创建5个进程
int i;
pthread_t tid[2]={0};
if(pthread_create(&tid[0],NULL,sale_ticket_a,NULL)!=0)
{
perror("pthread_create");
exit(1);
}
if(pthread_create(&tid[1],NULL,sale_ticket_b,NULL)!=0)
{
perror("pthread_create");
exit(1);
}
//等待和收回线程
void*status;
for(i=0;i<2;i++)
{
pthread_join(tid[i],&status);
}
//销毁互斥锁
pthread_mutex_destroy(&mutex);
//销毁条件变量
pthread_cond_destroy(&cond);
return 0;
}
运行结果
可以看到前50张只有一个线程在卖票
后50张后b线程才允许卖票
线程池(面试重点)
顾名思义就是一个“池子”里面有很多个线程,所以形象成线程池
在Linux中有两种池是非常实用的:线程池,内存池
这些池化被研发出来的目的就是为了提高程序运行的效率
我们需要使用内存的时候,在代码里面经常用malloc申请堆内存,它就是让系统在内存中找一块空闲的、连续的、满足需求的内存,这里面还涉及到遍历链表、数据结构等等操作,所以使用malloc的话运行效率会比较低。因此,有人就想在系统空闲的时候提前申请一块内存放在一个“池子”里面,需要用的时候就在里面拿一块出来,用完之后再放回去,这样就可以大大提高程序的运行效率,这就是所谓的“内存池”。
线程池也是一样的道理,如果我们频繁调用pthread_create()这个函数来创建线程的话,就比较浪费时间,尤其是在服务器端做开发的时候,就要频繁地启动线程,完了还得释放线程资源,这样比较影响运行效率。因此,有人想在服务器空闲的时候就申请几个线程放在一个“池子”里,然后使用pthread_cond_wait()函数让这些线程处于睡眠状态,等服务器端有任务过来的时候再用pthread_cond_signal()唤醒一个线程,等任务完成后再让它回去睡眠,也就是所谓的“线程池”的概念。
在池子里的线程个数由我们自己写代码决定,但是不宜过多。
我们创建线程池所需要的东西:
线程;
互斥锁;
条件变量;
任务队列;
一开始没有任务的时候队头和队尾指针都指向头结点
然后每来一个任务,rear就移动到最后一个结点
那比如现在进程有两个任务需要我们处理,我们就唤醒线程,执行完后再返回去睡眠
互斥锁是在访问队列的时候用,这个队列就是共享变量,主线程要不断地往里面放任务,子线程要不断地去里面取任务出来执行(调用函数,把函数名放在队列的结点里面,这个结点就是一个指针,指向了任务的函数,我们执行函数就通过函数指针去调用,除此之外,结点里面还要有一个arg指针,是给函数传参用的,结点里面还要有next这个指针,指向下一个节点),因为有人去放,有人去取,所以我们加锁,同一时刻只能有一个人来操作。
而条件变量就是用来控制线程什么时候应该启动,什么时候应该睡眠。
接下来我们要写的代码要同时用到这些技术:
线程;
互斥锁;
条件变量;
任务队列(链式队列,没有容量限制);
代码演示
第一步我们先定义队列和节点
第二步定义一个线程池,就是定义一个结构体,一个线程池里面最基本要包含这些东西:
接下来我们就创建线程池并初始化里面的所有成员
创建线程池create_thread_pool()
初始化线程池
注意:如果编译器报错的话,这个函数最后要加上return pool;//返回线程池。
线程处理函数(负责睡眠和获取任务)
然后是线程处理函数woker,线程的睡眠和线程去获取任务的工作在这个函数中进行,当执行到pthread_cond_wait()函数这里时,10个线程都会睡眠,程序都停在了这边。
更正:上图这里应该改成这样:因为应该是队头后面没有结点才算是空队
以上整个线程池的创建流程就大概完成了,我们可以打印一下线程池创建完成
(此时10个线程正在睡眠)然后主线程得在队列中添加任务,并且唤醒线程池中的线程执行任务(一个队列结点就是一个任务)
添加任务thread_pool_add()
唤醒线程池中的线程
程序唤醒后程序就来到了这边,此时线程还没有被关闭,所以直接从队列中取任务,然后执行
取任务并执行
执行完任务之后,如果队列为空并且线程池没有被关闭的话线程就又进入睡眠
重新进入睡眠
一个线程大概要5s,10个线程同时被唤醒,所以5s之后10个线程都又睡眠了
目前为止代码运行的结果就是一次大概能唤醒10个线程
......
销毁线程池thread_pool_destroy()
剩下的任务就是销毁线程池,操作的流程基本是创建线程池的倒着来的
一旦关闭线程池的标志位等于1的时候,线程又被唤醒
然后10个线程都执行了这段代码:
运行结果
执行完毕后关闭线程,退出成功
完整代码:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
//表示队列节点的结构体
typedef struct Task
{
void (*function)(void *arg);
void *arg;
struct Task *next;
}Task;
//表示线程池
typedef struct ThreadPool
{
//任务队列
Task *queueFront;//指向队头节点的指针
Task *queueRear;//指向队尾节点的指针
//线程的数量
int num;
//线程号
pthread_t *threadID;
//互斥锁和条件变量
pthread_mutex_t mutex;
pthread_cond_t cond;
//关闭线程池的标志位
int shutdown;//0不关,1关
}ThreadPool;
//线程处理函数
void *worker(void*arg)
{
ThreadPool *pool=(ThreadPool*)arg;
//因为线程池里面的线程再处理完任务之后不是结束,而是要回去睡眠,所以它不能结束,因此要死循环
while(1)
{
//访问队列里面的任务时,先上锁
pthread_mutex_lock(&pool->mutex);
//访问任务队列--相当于共享变量
while(pool->queueFront==pool->queueRear && pool->shutdown==0)//如果是空队并且线程池还没有关闭,则线程睡眠
{
pthread_cond_wait(&pool->cond,&pool->mutex);//拿到条件变量则睡眠并解锁
}
//如果线程池被关闭
if(pool->shutdown==1)
{
//退出之前要解锁
pthread_mutex_unlock(&pool->mutex);
printf("线程池被关闭 线程 %ld 退出...\n",pthread_self());
pthread_exit((void*)0);//可以看到线程退出的状态
}
//从任务队列获取一个任务(出队),并执行
Task task;
Task *t=pool->queueFront->next;//把要出队的节点的位置记下来
task.function=t->function;//把要出队的节点的任务函数记下来
task.arg=t->arg;//把要出队的节点想要传给任务函数的参数记下来
pool->queueFront->next=t->next;//将出队的节点的下一个节点的位置给到头节点的指针域
free(t);//释放要出队的节点
if(pool->queueFront->next==NULL)
{
pool->queueRear=pool->queueFront;//如果队头节点的后面没有节点了,就让队头队尾指针指向同一个节点
}
//释放互斥锁
pthread_mutex_unlock(&pool->mutex);
//执行任务
printf("thread %ld start working...\n",pthread_self());
task.function(task.arg); //通过函数指针调用函数
printf("thread %ld end working...\n",pthread_self());
}
}
//创建线程池函数
ThreadPool *create_thread_pool(int num)
{
//给线程池申请一块空间,即申请线程池结构体
ThreadPool *pool=(ThreadPool*)malloc(sizeof(ThreadPool));
if(NULL==pool)
{
fprintf(stderr,"malloc ThreadPool failure\n");
return NULL;
}
//初始化线程池里面的成员
//1.初始化任务队列
pool->queueFront=(Task*)malloc(sizeof(Task));//给队头申请一个节点
if(NULL==pool->queueFront)
{
fprintf(stderr,"malloc Task failure\n");
free(pool);//如果申请失败直接释放掉整个池
return NULL;
}
pool->queueRear=pool->queueFront;//让队尾也指向队头节点
pool->queueFront->next=NULL;//队头后面还没有节点
//2.初始化线程的数量
pool->num=num;
//3.初始化线程号
pool->threadID=(pthread_t*)malloc(sizeof(pthread_t)*num);
if(NULL==pool->threadID)
{
fprintf(stderr,"malloc pthread_t failure\n");
free(pool->queueFront);//如果失败先释放节点空间,再释放结构体
free(pool);
return NULL;
}
//4.初始化线程
int i;
for(i=0;i<num;i++)
{
//创建线程
if(pthread_create(&pool->threadID[i],NULL,worker,pool)!=0)//把线程池传过去给线程函数worker
{
fprintf(stderr,"pthread_create failure\n");
free(pool->queueFront);
free(pool->threadID);
free(pool);
return NULL;
}
//等待和回收线程
pthread_detach(pool->threadID[i]);//线程运行结束后自动释放资源
}
//5.初始化互斥锁和条件变量
pthread_mutex_init(&pool->mutex,NULL);
pthread_cond_init(&pool->cond,NULL);
//初始化关闭线程池的标志位
pool->shutdown=0;//0表示不关,1表示关
return pool;
}
//任务函数
void taskfunc(void *arg)
{
int num=*(int*)arg;
printf("thread %ld is working num=%d ...\n",pthread_self(),num);
sleep(1);
free(arg);
}
//把任务添加到任务队列里面
void thread_pool_add(ThreadPool*pool,void(*func)(void*),void*arg)
{
//主线程在往任务队列中放数据的时候需要上锁
pthread_mutex_lock(&pool->mutex);
//进队操作
Task*t=(Task*)malloc(sizeof(Task));//申请节点
if(NULL==t)
{
fprintf(stderr,"malloc Task failure\n");
return;//结束函数
}
//往节点里面填东西
t->function=func;//将taskfunc函数的地址赋值给function函数
t->arg=arg;//要传给function函数的参数
t->next=NULL;//尾插法后面没有节点
//把新的节点的地址放到上一个节点的指针域里面
pool->queueRear->next=t;
//让rear指向新的节点
pool->queueRear=t;
//放好数据之后要解锁
pthread_mutex_unlock(&pool->mutex);
//进队操作结束要唤醒一个线程去取出这个任务(让它出队)
pthread_cond_signal(&pool->cond);//把条件变量广播出去,哪个线程取得它,哪个线程就被唤醒
}
//销毁线程池
void thread_pool_destroy(ThreadPool*pool)
{
//关闭线程池
pool->shutdown=1;
//唤醒10个线程
int i;
for(i=0;i<pool->num;i++)
{
pthread_cond_signal(&pool->cond);
}
//释放线程号
if(pool->threadID)
free(pool->threadID);
//释放任务队列
while(pool->queueFront->next)//只要队头后面还有节点,就一直释放
{
Task*t=pool->queueFront->next;//t指向第一个节点
pool->queueFront->next=t->next;//将第二个节点的位置放到队头的指针域
free(t);//释放第一个节点
}
free(pool->queueFront); //最后释放头节点
//销毁互斥量和条件变量
pthread_mutex_destroy(&pool->mutex);
pthread_cond_destroy(&pool->cond);
//释放线程结构体
free(pool);
}
int main()
{
//创建线程池
ThreadPool *pool=create_thread_pool(10);//把需要的线程数量传过去
if(NULL==pool)
{
return -1;
}
printf("线程池创建完成\n");
sleep(1);
//主线程往任务队列中添加任务,并且唤醒线程池中的线程
int i;
for(i=0;i<50;i++)//50个任务,10个线程,那就是1个线程处理5个任务,一个任务1s,一个线程总共需要5s
{
int *n=(int*)malloc(sizeof(int));
*n=i; //n指向了存放了i的一个空间
//把任务添加到任务队列里面
thread_pool_add(pool,taskfunc,n);
}
sleep(6);//1个任务大概要1s,一个线程大概需要5s,这里睡眠只要比5大点就行
//销毁线程池
thread_pool_destroy(pool);
return 0;
}
下节开始学习网络编程!
本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓