嵌入式全栈开发学习笔记---Linux系统编程(多线程编程)

news2024/11/25 11:54:50

目录

多线程

线程(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;
}

下节开始学习网络编程!

本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2092908.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Code Practice Journal | Day59-60_Graph09 最短路径(待更)

1. Dijkstra 1.1 原理与步骤 步骤&#xff1a; 选取距离源点最近且未被访问过的节点标记该节点为已访问更新未访问节点到源点的距离 1.2 代码实现 以KamaCoder47题为例 题目&#xff1a;47. 参加科学大会&#xff08;第六期模拟笔试&#xff09; (kamacoder.com) class Progra…

非开发者也能体验纯血鸿蒙了?15款机型开启鸿蒙NEXT测试招募

非开发者也能体验纯血鸿蒙了&#xff1f;这15款机型正式开启鸿蒙NEXT Beta版测试招募 8月26日&#xff0c;华为发起了新一轮的HarmonyOS NEXT Beta版的升级招募&#xff0c;据了解&#xff0c;此次升级招募面向华为Mate 60系列、华为Mate X5&#xff08;含典藏版&#xff09;、…

Linux: make指令与Makefile文件

目录 1. 场景介绍 2. make指令与Makefile文件 3. 依赖关系和依赖方法 4. 项目清理 5. 原理 5.1 识别文件是否需要编译 5.2 make原理 6. 扩展 总结 1. 场景介绍 Linux操作系统中&#xff0c;我们每编写一个C/C代码&#xff0c;都要手动使用gcc/g指令&#xff0c;对代码…

ffmpeg源码分析(六)内存管理

系列文章目录 FFmpeg源码解析系列&#xff08;一&#xff09;目录和编译 FFmpeg源码解析系列&#xff08;二&#xff09;主要结构体 ffmpeg源码解析系列&#xff08;四&#xff09;结构体之AVIOContext 、URLContext、URLProtocol ffmpeg源码解析系列&#xff08;五&#xff0…

phpstudy怎么用

启动Apache 这是你的默认网站域名。点击物理路径 进入到目录&#xff0c;将你的php文件项目拖进去。如test.php 打开浏览器

python 天气与股票的关系--第2部分,清洗数据

先看一下股票信息 合并天气信息 合并2个数据 合并之后&#xff0c;会自动删除 周六和周日 节假日 下一篇&#xff0c;尝试建立数学模型

生产环境变态开启devtools(redux篇)

前沿 默认都安装了谷歌的redux-devtools插件哦 没有亮,说明关闭了生产环境的redux devtools工具, 接下来跟着博主一起变态启用它 如果看了我上一篇的小伙伴,应该会很熟练了,如果没有看上一篇的,也没关系,博主会手摸手的教你们打开它。 正常的解决方案(适用内部开发人员…

【c++】日期类相关实践:计算日期到天数转换、日期差值

相关文章&#xff1a;日期类&#xff08;运算符重载应用&#xff09;详细版 目录 前言 实践1&#xff1a;计算日期到天数转换 题目 方法 关键代码 完整代码 实践2&#xff1a;日期差值 题目 方法 关键代码 完整代码 &#x1f497;感谢阅读&#xff01;&#x1f49…

python网络爬虫(四)——实战练习

0.为什么要学习网络爬虫 深度学习一般过程:   收集数据&#xff0c;尤其是有标签、高质量的数据是一件昂贵的工作。   爬虫的过程&#xff0c;就是模仿浏览器的行为&#xff0c;往目标站点发送请求&#xff0c;接收服务器的响应数据&#xff0c;提取需要的信息&#xff0c…

Python 算法交易实验85 QTV200日常推进-钳制指标与交易量

说明 继续保持思考与尝试 最近挺有意思的&#xff0c;碰到很多技术上的问题&#xff0c;其解决方案都类似“阴阳两仪”的概念。 "阴阳两仪"是中国古代哲学中的一个重要概念&#xff0c;源自《易经》&#xff08;又称《周易》&#xff09;。它是对宇宙间最基本对立统一…

数据结构与算法 第5天(树和二叉树)

树形结构 一对多 只有一个前驱 可以有多个后继 树的定义 基本术语 有序树&#xff1a;树中结点的各子树从左至右有次序(最左边的为第一个孩子) 森林&#xff1a;是 m(m≥0)棵互不相交的树的集合。 一棵树可以看成特殊的森林 二叉树 每个节点最多有两个…

【王树森】BERT:预训练Transformer模型(个人向笔记)

前言 BERT&#xff1a;Bidirectional Encoder Representations from TransformerBERT是用来预训练Transformer模型的encoder的本节课只讲述主要思想BERT用两个主要思想来训练Transformer的encoder网络&#xff1a;①随机遮挡单词&#xff0c;让encoder根据上下文来预测被遮挡的…

2024年9月1日 十二生肖 今日运势

小运播报&#xff1a;2024年9月1日&#xff0c;星期日&#xff0c;农历七月廿九 &#xff08;甲辰年壬申月戊辰日&#xff09;&#xff0c;法定节假日。 红榜生肖&#xff1a;鸡、猴、鼠 需要注意&#xff1a;龙、兔、狗 喜神方位&#xff1a;东南方 财神方位&#xff1a;正…

【系统架构设计师-2015年】综合知识-答案及详解

【第1~2题】 某航空公司机票销售系统有n个售票点&#xff0c;该系统为每个售票点创建一个进程Pi&#xff08;i1&#xff0c;2&#xff0c;…&#xff0c;n&#xff09;管理机票销售。假设Tj&#xff08;j1&#xff0c;2&#xff0c;…&#xff0c;m&#xff09;单元存放某日某…

2025届必看:如何用Java SpringBoot+Vue打造免费体育馆场地预约系统?

✍✍计算机毕业编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java、…

异常与使用

异常 一、C语言传统的错误处理机制二、异常1、概念2、关键字3、示例 三、异常的使用1、异常的抛出和匹配原则2、在函数调用链中异常栈展开匹配原则3、栈展开示意图4、示例代码5、运行结果 四、异常的重新抛出1、作用2、示例代码3、运行结果 五、异常安全六、异常规范1、概念2、…

CSS-浮动【看这一篇就够了】

目录 浮动布局 浮动是如何工作的 浮动的本质和要点 如何产生浮动 元素浮动的特性 1.元素添加浮动后&#xff0c;脱离文档流 2.如果父元素的宽度不够 3.浮动的顺序贴靠特性 4.元素浮动后&#xff0c;具有行内块级元素特性 5.浮动的元素会造成父元素高度塌陷 6.浮动对…

“无法连接打印机0X0000011B”原因分析及多种解决方法

在日常办公和生活中&#xff0c;打印机是不可或缺的重要设备。然而&#xff0c;有时在连接打印机的过程中&#xff0c;我们可能会遇到错误代码0x0000011b的提示。有更新补丁导致的、有访问共享打印机服务异常、有访问共享打印机驱动异常等问题导致的&#xff0c;针对访问共享打…

MySQL场景测试题

第一题 软件环境描述&#xff1a; Mysql V5.7.30 Innodb RR隔离级别 表结构以及数据描述&#xff1a; &#xff08;1&#xff09;t_user用户表&#xff0c;表格如下&#xff1a; CREATE TABLE t_user ( id int(10) NOT NULL, name varchar(100) DEFAULT NULL, PRIMARY KEY (id)…

240831-Gradio之RAG文档对话工具Kotaemon的安装与配置

A. 用户界面 该项目既可以作为功能性 RAG UI&#xff0c;既可以用于对文档进行 QA 的最终用户&#xff0c;也可以用作想要构建自己的 RAG 管道的开发人员。对于最终用户&#xff1a; - 一个干净且简约的用户界面&#xff0c;用于基于RAG的QA。 - 支持 LLM API 提供程序&#xf…