多线程编程

1 线程的使用

1.1 为什么要使用多线程

        在编写代码时,是否会遇到以下的场景会感觉到难以下手?
        要做 2 件事,一件需要阻塞等待,另一件需要实时进行。例如播放器:一边在屏幕上播放视频,一边在等待用户的按键操作。如果使用单线程的话,程序必须一会查询有无按键,一会播放视频。查询按键太久,就会导致视频播放卡顿;视频播放太久,就无法及时响应用户的操作。并且查询按键和播放视频的代码混杂在一起,代码丑陋。
        如果使用多线程,线程 1 单独处理按键,线程 2 单独处理播放,可以完美解决上述问题。

1.2 线程概念        

        所谓线程,就是操作系统所能调度的最小单位。普通的进程,只有一个线程在执行对应的逻辑。我们可以通过多线程编程,使一个进程可以去执行多个不同的任务。相比多进程编程而言,线程享有共享资源,即在进程中出现的全局变量,每个线程都可以去访问它,与进程共享“4G ”内存空间,使得系统资源消耗减少。本章节来讨论 Linux POSIX 线程。

1.3 线程的标识 pthread_t

        对于进程而言,每一个进程都有一个唯一对应的 PID 号来表示该进程,而对于线程而言,也有一个“类似于进程的 PID 号”,名为 tid ,其本质是一个pthread_t 类型的变量。线程号与进程号是表示线程和进程的唯一标识,但是对于线程号而言,其仅仅在其所属的进程上下文中才有意义。
获取线程号
#include <pthread.h>
pthread_t pthread_self(void);
成功:返回线程号
        在程序中,可以通过函数 pthread_self ,来返回当前线程的线程号,例程 1 给出了打印线程 tid 号。

测试例程 1

#include <pthread.h>
#include <stdio.h>

int main()
{
	pthread_t tid = pthread_self();
	printf("tid = %lu\n",(unsigned long)tid);
	return 0;
}
注意: 因采用 POSIX 线程接口,故在要编译的时候包含 pthread 库,使用 gcc 编译应 gcc xxx.c -lpthread 方可编译多线程程序。

编译结果:

1.4 线程的创建

        怎么创建线程呢?使用 pthread_create 函数:

创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
该函数第一个参数为 pthread_t 指针,用来保存新建线程的线程号;
第二个参数表示了线程的属性,一般传入 NULL 表示默认属性;
第三个参数是一个函数指针,就是线程执行的函数。这个函数返回值为 void* ,形参为 void*
第四个参数则表示为向线程处理函数传入的参数,若不传入,可用 NULL 填充,有关线程传参后续小节会有详细的说明,接下来通过一个简单例程来使用该函数创建出一个线程。
测试例程 2:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

void *fun(void *arg)
{
	printf("pthread_New = %lu\n",(unsigned long)pthread_self());
}

int main()
{

	pthread_t tid1;
	int ret = pthread_create(&tid1,NULL,fun,NULL);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}

	/*tid_main 为通过pthread_self获取的线程ID,tid_new通过执行pthread_create成功后tid指向的空间*/
	printf("tid_main = %lu tid_new = %lu \n",(unsigned long)pthread_self(),(unsigned long)tid1);
	
	/*因线程执行顺序随机,不加sleep可能导致猪线程先执行,导致进程结束,无法执行到子线程*/
	sleep(1);

	return 0;
}
运行结果:

        通 过 pthread_create 确 实 可 以 创 建 出 来 线 程 , 主 线 程 中 执 行 pthread_create 后 的 tid 指向了线程号空间,与子线程通过函数 pthread_self 打印出来的线程号一致。
        特别说明的是,当主线程伴随进程结束时,所创建出来的线程也会立即结束,不会继续执行。并且创建出来的线程的执行顺序是随机竞争的,并不能保证哪一个线程会先运行。可以将上述代码中 sleep 函数进行注释,观察实验现象。
        去掉上述代码 25 行后运行结果: .

        上述运行代码 3 次,其中有 2 次被进程结束,无法执行到子线程的逻辑,最后一 次则执行到了子线程逻辑后结束的进程。如此可以说明,线程的执行顺序不受控制,且整个进程结束后所产生的线程也随之被释放,在后续内容中将会描述如何控制线程执行。

1.5 向线程传入参数

        pthread_create()的最后一个参数的为 void* 类型的数据,表示可以向线程传递一个 void* 数据类型的参数,线程的回调函数中可以获取该参数,例程 3 举例了如何向线程传入变量地址与变量值。
测试例程 3
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

void *fun1(void *arg)
{
	printf("%s:arg = %d Addr = %p\n",__FUNCTION__,*(int *)arg,arg);
}

void *fun2(void *arg)
{
	printf("%s:arg = %d Addr = %p\n",__FUNCTION__,(int)(long)arg,arg);
}

int main()
{

	pthread_t tid1,tid2;
	int a = 50;
	int ret = pthread_create(&tid1,NULL,fun1,(void *)&a);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	sleep(1);
	printf("%s:a = %d Add = %p \n",__FUNCTION__,a,&a);
	return 0;
}
运行结果:

        本例程展示了如何利用线程创建函数的第四个参数向线程传入数据,举例了如何以地址的方式传入值、以变量的方式传入值,例程代码的 21 行,是将变量 a 先行取地址后,再次强制类型转化为 void* 后传入线程,线程处理的回调函数中,先将万能指针 void* 转化为 int* ,再次取地址就可以获得该地址变量的值,其本质在于地址的传递。例程代码的 27 行,直接将 int 类型的变量强制转化为 void*进行传递(针对不同位数机器,指针对其字数不同,需要 int 转化为 long 在转指针,否则可能会发生警告),在线程处理回调函数中,直接将 void*数据转化为 int 类型即可,本质上是在传递变量 a 的值。
        上述两种方法均可得到所要的值,但是要注意其本质,一个为地址传递,一个为值的传递。当变量发生改变时候,传递地址后,该地址所对应的变量也会发生改变,但传入变量值的时候,即使地址指针所指的变量发生变化,但传入的为变量值,不会受到指针的指向的影响,实际项目中切记两者之间的区别。具体说明见例程 4

测试例程 4:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

void *fun1(void *arg)
{
	while(1){
	
		printf("%s:arg = %d Addr = %p\n",__FUNCTION__,*(int *)arg,arg);
		sleep(1);
	}
}

void *fun2(void *arg)
{
	while(1){
	
		printf("%s:arg = %d Addr = %p\n",__FUNCTION__,(int)(long)arg,arg);
		sleep(1);
	}
}

int main()
{

	pthread_t tid1,tid2;
	int a = 50;
	int ret = pthread_create(&tid1,NULL,fun1,(void *)&a);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	sleep(1);
	ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	while(1){
		a++;
		sleep(1);
		printf("%s:a = %d Add = %p \n",__FUNCTION__,a,&a);
	}
	return 0;
}

运行结果:

        上述例程讲述了如何向线程传递一个参数,在处理实际项目中,往往会遇到传递多个参数的问题,我们可以通过结构体来进行传递,解决此问题。
测试例程 5
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

struct Stu{
	int Id;
	char Name[32];
	float Mark;
};

void *fun1(void *arg)
{
	struct Stu *tmp = (struct Stu *)arg;
	printf("%s:Id = %d Name = %s Mark = %.2f\n",__FUNCTION__,tmp->Id,tmp->Name,tmp->Mark);
	
}

int main()
{

	pthread_t tid1,tid2;
	struct Stu stu;
	stu.Id = 10000;
	strcpy(stu.Name,"ZhangSan");
	stu.Mark = 94.6;

	int ret = pthread_create(&tid1,NULL,fun1,(void *)&stu);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	printf("%s:Id = %d Name = %s Mark = %.2f\n",__FUNCTION__,stu.Id,stu.Name,stu.Mark);
	sleep(1);
	return 0;
}
运行结果:

1.6 线程的退出与回收

        线程的退出情况有三种:第一种是进程结束,进程中所有的线程也会随之结束。第二种是通过函数 pthread_exit 来主动的退出线程。第三种被其他线程调 用 pthread_cancel 来被动退出。
        线程结束后,主线程可以通过函数 pthread_join/pthread_tryjoin_np 来回收线程的资源,并且获得线程结束后需要返回的数据。

1.6.1 线程主动退出

pthread_exit 函数原型如下:
线程主动退出
#include <pthread.h>
void pthread_exit(void *retval);
        pthread_exit 函数为线程退出函数,在退出时候可以传递一个 void* 类型的数据带给主线程,若选择不传出数据,可将参数填充为 NULL

1.6.2 线程被动退出

线程被动退出,其他线程使用该函数让另一个线程退出
#include <pthread.h>
int pthread_cancel(pthread_t thread);
成功:返回 0
        该函数传入一个 tid 号,会强制退出该 tid 所指向的线程,若成功执行会返回 0

1.6.3 线程资源回收(阻塞方式)

线程资源回收(阻塞)
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

        该函数为线程回收函数,默认状态为阻塞状态,直到成功回收线程后才返回。第一个参数为要回收线程的 tid 号,第二个参数为线程回收后接受线程传出的数据。

1.6.4 线程资源回收(非阻塞方式)

pthread_tryjoin_np 函数原型如下:
线程资源回收(非阻塞)
#define _GNU_SOURCE 
#include <pthread.h>
int pthread_tryjoin_np(pthread_t thread, void **retval);
        该函数为非阻塞模式回收函数,通过返回值判断是否回收掉线程,成功回收则返回 0 ,其余参数与 pthread_join 一致。

测试例程 6

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

void *fun1(void *arg)
{
	static int tmp = 0;//必须要static修饰,否则pthread_join无法获取到正确值
	//int tmp = 0;
	tmp = *(int *)arg;
	tmp+=100;
	printf("%s:Addr = %p tmp = %d\n",__FUNCTION__,&tmp,tmp);
	pthread_exit((void *)&tmp);
}


int main()
{

	pthread_t tid1;
	int a = 50;
	void *Tmp = NULL;
	int ret = pthread_create(&tid1,NULL,fun1,(void *)&a);//将变量以地址的形式传入线程,在线程中做出了自加100 的操作
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	pthread_join(tid1,&Tmp);
	printf("%s:Addr = %p Val = %d\n",__FUNCTION__,Tmp,*(int *)Tmp);
	return 0;
}
运行结果:

        上述例程先通过 23 行将变量以地址的形式传入线程,在线程中做出了自加100 的操作,当线程退出的时候通过线程传参,用 void* 类型的数据通过 pthread_join 接 受 。 此 例 程 去 掉 了 之 前 加 入 的 sleep 函 数 , 原 因 是 pthread_join 函数具备阻塞的特性,直至成功收回掉线程后才会冲破阻塞,因此不需要靠考虑主线程会执行到 30 行结束进程的情况。特别要说明的是例程第 8 行,当变量从线程传出的时候,需要加 static 修饰,对生命周期做出延续,否则无法传出正确的变量值。
测试例程 7
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

struct Stu{
	int Id;
	char Name[32];
	float Mark;
};

void *fun1(void *arg)
{
	struct Stu *tmp = (struct Stu *)arg;
	printf("%s:Id = %d Name = %s Mark = %.2f\n",__FUNCTION__,tmp->Id,tmp->Name,tmp->Mark);
	
}

int main()
{

	pthread_t tid1,tid2;
	struct Stu stu;
	stu.Id = 10000;
	strcpy(stu.Name,"ZhangSan");
	stu.Mark = 94.6;

	int ret = pthread_create(&tid1,NULL,fun1,(void *)&stu);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	printf("%s:Id = %d Name = %s Mark = %.2f\n",__FUNCTION__,stu.Id,stu.Name,stu.Mark);
	sleep(1);
	return 0;
}
运行结果:

        例程 7 展示了如何使用非阻塞方式来回收线程,此外也展示了多个线程可以指向同一个回调函数的情况。例程 6 通过阻塞方式回收线程几乎规定了线程回收的顺序,若最先回收的线程未退出,则一直会被阻塞,导致后续先退出的线程无法及时的回收。
        通过函数 pthread_tryjoin_np ,使用非阻塞回收,线程可以根据退出先后顺序自由的进行资源的回收。
测试例程 8
#define _GNU_SOURCE 
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

void *fun1(void *arg)
{
	printf("Pthread:1 come!\n");
	while(1){
		sleep(1);
	}
}

void *fun2(void *arg)
{
	printf("Pthread:2 come!\n");
	pthread_cancel((pthread_t )(long)arg);
	pthread_exit(NULL);
}

int main()
{
	int ret,i,flag = 0;
	void *Tmp = NULL;
	pthread_t tid[2];
	ret = pthread_create(&tid[0],NULL,fun1,NULL);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	sleep(1);
	ret = pthread_create(&tid[1],NULL,fun2,(void *)tid[0]);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	while(1){
		for(i = 0;i <2;i++){
			if(pthread_tryjoin_np(tid[i],NULL) == 0){
				printf("Pthread : %d exit !\n",i+1);
				flag++;	
			}
		}
		if(flag >= 2) break;
	}
	return 0;
}
运行结果:

        例程 8 展示了如何利用 pthread_cancel 函数主动的将某个线程结束。 27 行与 33 行创建了线程,将第一个线程的线程号传参形式传入了第二个线程。第一个的线程执行死循环睡眠逻辑,理论上除非进程结束,其永远不会结束,但在第二个线程中调用了 pthread_cancel 函数,相当于向该线程发送一个退出的指令,导致线程被退出,最终资源被非阻塞回收掉。此例程要注意第 32 行的 sleep 函数,一定要确保线程 1 先执行,因线程是无序执行,故加入该睡眠函数控制顺序,在本章后续,会讲解通过加锁、信号量等手段来合理的控制线程的临界资源访问与线程执行顺序控制。

2 线程的控制

2.1 多线程编临界资源访问

        当线程在运行过程中,去操作公共资源,如全局变量的时候,可能会发生彼此“矛盾”现象。例如线程 1 企图想让变量自增,而线程 2 企图想要变量自减,两个线程存在互相竞争的关系导致变量永远处于一个“平衡状态”,两个线程互相竞争,线程 1 得到执行权后将变量自加,当线程 2 得到执行权后将变量自减,变量似乎永远在某个范围内浮动,无法到达期望数值,如例程 9 所示。
测试例程 9
#define _GNU_SOURCE 
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>


int Num = 0;

void *fun1(void *arg)
{
	while(Num < 3){
		Num++;
		printf("%s:Num = %d\n",__FUNCTION__,Num);
		sleep(1);
	}
	pthread_exit(NULL);
}

void *fun2(void *arg)
{
	while(Num > -3){
		Num--;
		printf("%s:Num = %d\n",__FUNCTION__,Num);
		sleep(1);
	}
	pthread_exit(NULL);
}

int main()
{
	int ret;
	pthread_t tid1,tid2;
	ret = pthread_create(&tid1,NULL,fun1,NULL);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	ret = pthread_create(&tid2,NULL,fun2,NULL);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
	return 0;
}
运行结果:

        为了解决上述对临界资源的竞争问题,pthread 线程引出了互斥锁来解决临界资源访问。通过对临界资源加锁来保护资源只被单个线程操作,待操作结束后解锁,其余线程才可获得操作权。

2.2 互斥锁 API 简述

        多个线程都要访问某个临界资源,比如某个全局变量时,需要互斥地访问:我访问时,你不能访问。
        可以使用以下函数进行互斥操作。

2.2.1 初始化互斥量

函数原型如下:
int pthread_mutex_init(phtread_mutex_t *mutex, const pthread_mutexattr_t *restrict attr)
        该函数初始化一个互斥量,第一个参数是改互斥量指针,第二个参数为控制互斥量的属性,一般为 NULL 。当函数成功后会返回 0 ,代表初始化互斥量成功。
        当然初始化互斥量也可以调用宏来快速初始化,代码如下:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;

2.2.2 互斥量加锁/解锁

函数原型如下:
互斥量加锁(阻塞)/解锁
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
成功:返回 0
        lock 函数与 unlock 函数分别为加锁解锁函数,只需要传入已经初始化好的 pthread_mutex_t 互斥量指针。成功后会返回 0
        当某一个线程获得了执行权后,执行 lock 函数,一旦加锁成功后,其余线程遇到 lock 函数时候会发生阻塞,直至获取资源的线程执行 unlock 函数后。unlock 函数会唤醒其他正在等待互斥量的线程。
        特别注意的是,当获取 lock 之后,必须在逻辑处理结束后执行 unlock ,否则会发生死锁现象!导致其余线程一直处于阻塞状态,无法执行下去。在使用互斥量的时候,尤其要注意使用 pthread_cancel 函数,防止发生死锁现象!

2.2.3 互斥量加锁(非阻塞方式)

函数原型如下:
互斥量加锁(非阻塞)
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
        该函数同样也是一个线程加锁函数,但该函数是非阻塞模式通过返回值来判断是否加锁成功,用法与上述阻塞加锁函数一致。

2.2.4 互斥量加锁(非阻塞方式)

函数原型如下:
互斥量销毁
#include <pthread.h>
int pthread_mutex_destory(pthread_mutex_t *mutex);
成功:返回 0
        该函数是用于销毁互斥量的,传入互斥量的指针,就可以完成互斥量的销毁,成功返回 0

2.2.5 程序示例

#define _GNU_SOURCE 
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

pthread_mutex_t mutex;//互斥量变量 一般申请全局变量

int Num = 0;//公共临界变量

void *fun1(void *arg)
{
	pthread_mutex_lock(&mutex);//加锁 若有线程获得锁,则会阻塞
	while(Num < 3){
		Num++;
		printf("%s:Num = %d\n",__FUNCTION__,Num);
		sleep(1);
	}
	pthread_mutex_unlock(&mutex);//解锁
	pthread_exit(NULL);//线程退出 pthread_join 会回收资源
}

void *fun2(void *arg)
{
	pthread_mutex_lock(&mutex);加锁 若有线程获得锁,则会阻塞
	while(Num > -3){
		Num--;
		printf("%s:Num = %d\n",__FUNCTION__,Num);
		sleep(1);
	}
	pthread_mutex_unlock(&mutex);//解锁
	pthread_exit(NULL);//线程退出 pthread_join 会回收资源
}

int main()
{
	int ret;
	pthread_t tid1,tid2;
	ret = pthread_mutex_init(&mutex,NULL);//初始化互斥量
	if(ret != 0){
		perror("pthread_mutex_init");
		return -1;
	}
	ret = pthread_create(&tid1,NULL,fun1,NULL);//创建线程 1
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	ret = pthread_create(&tid2,NULL,fun2,NULL);//创建线程 2
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	pthread_join(tid1,NULL);//阻塞回收线程 1
	pthread_join(tid2,NULL);//阻塞回收线程 2
	pthread_mutex_destroy(&mutex);//销毁互斥量
	return 0;
}
运行结果:
        上述例程通过加入互斥量,保证了临界变量某一时刻只被某一线程控制,实现了临界资源的控制。需要说明的是,线程加锁在循环内与循环外的情况。本历程在进入 while 循环前进行了加锁操作,在循环结束后进行的解锁操作,如果将加锁解锁全部放入 while 循环内,作为单核的机器,执行结果无异,当有多核机器执行代码时,可能会发生“抢锁”现象,这取决于操作系统底层的实现。

2.3 多线程编执行顺序控制

        解决了临界资源的访问,但似乎对线程的执行顺序无法得到控制,因线程都是无序执行,之前采用 sleep 强行延时的方法勉强可以控制执行顺序,但此方法在实际项目情况往往是不可取的,其仅仅可解决线程创建的顺序,当创建之后执行的顺序又不会受到控制,于是便引入了信号量的概念,解决线程执行 顺序。
测试例程 11
#define _GNU_SOURCE 
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

void *fun1(void *arg)
{
	printf("%s:Pthread Come!\n",__FUNCTION__);
	pthread_exit(NULL);
}

void *fun2(void *arg)
{
	printf("%s:Pthread Come!\n",__FUNCTION__);
	pthread_exit(NULL);
}

void *fun3(void *arg)
{
	printf("%s:Pthread Come!\n",__FUNCTION__);
	pthread_exit(NULL);
}

int main()
{
	int ret;
	pthread_t tid1,tid2,tid3;
	ret = pthread_create(&tid1,NULL,fun1,NULL);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	ret = pthread_create(&tid2,NULL,fun2,NULL);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	ret = pthread_create(&tid3,NULL,fun3,NULL);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
	pthread_join(tid3,NULL);
	return 0;
}
运行结果:

        通过上述例程可以发现,多次执行该函数其次序是无序的,线程之间的竞争无法控制,通过使用信号量来使得线程顺序为可控的。

2.4 信号量 API 简述

        注意:信号量跟互斥量不一样,互斥量用来防止多个线程同时访问某个临界资源。信号量起通知作用,线程 A 在等待某件事,线程 B 完成了这件事后就可以给线程 A 发信号。

2.4.1 初始化信号量

函数原型如下:
int sem_init(sem_t *sem,int pshared,unsigned int value);
该函数可以初始化一个信号量,第一个参数传入 sem_t 类型指针;
第二个参数传入 0 代表线程控制,否则为进程控制;
第三个参数表示信号量的初始值, 0 代表阻塞, 1 代表运行。
待初始化结束信号量后,若执行成功会返回 0

2.4.2 信号量 P/V 操作

函数原型如下:
#include <pthread.h>
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
成功:返回 0
sem_wait 函数作用为检测指定信号量是否有资源可用,若无资源可用会阻塞等待,若有资源可用会自动的执行“sem-1 ”的操作。所谓的“ sem-1 ”是与上述初始化函数中第三个参数值一致,成功执行会返回 0
sem_post 函数会释放指定信号量的资源,执行“ sem+1 ”操作。通过以上 2 个函数可以完成所谓的 PV 操作,即信号量的申请与释放,完成对线程执行顺序的控制。

2.4.3 信号量申请(非阻塞方式)

函数原型如下:
#include <pthread.h>
int sem_trywait(sem_t *sem);
成功:返回 0
        此函数是信号量申请资源的非阻塞函数,功能与 sem_wait 一致,唯一区别在于此函数为非阻塞。

2.4.4 信号量销毁

函数原型如下:
#include <pthread.h>
int sem_destory(sem_t *sem);
成功:返回 0
        该函数为信号量销毁函数,执行过后可将信号量进行销毁。

2.4.5 程序示例

测试例程 12
#define _GNU_SOURCE 
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <semaphore.h>

sem_t sem1,sem2,sem3;//申请的三个信号量变量

void *fun1(void *arg)
{
	sem_wait(&sem1);//因sem1本身有资源,所以不被阻塞 获取后sem1-1 下次会会阻塞
	printf("%s:Pthread Come!\n",__FUNCTION__);
	sem_post(&sem2);// 使得sem2获取到资源
	pthread_exit(NULL);
}

void *fun2(void *arg)
{
	sem_wait(&sem2);//因sem2在初始化时无资源会被阻塞,直至14行代码执行 不被阻塞 sem2-1 下次会阻塞
	printf("%s:Pthread Come!\n",__FUNCTION__);
	sem_post(&sem3);// 使得sem3获取到资源
	pthread_exit(NULL);
}

void *fun3(void *arg)
{
	sem_wait(&sem3);//因sem3在初始化时无资源会被阻塞,直至22行代码执行 不被阻塞 sem3-1 下次会阻塞
	printf("%s:Pthread Come!\n",__FUNCTION__);
	sem_post(&sem1);// 使得sem1获取到资源
	pthread_exit(NULL);
}

int main()
{
	int ret;
	pthread_t tid1,tid2,tid3;
	ret = sem_init(&sem1,0,1);  //初始化信号量1 并且赋予其资源
	if(ret < 0){
		perror("sem_init");
		return -1;
	}
	ret = sem_init(&sem2,0,0); //初始化信号量2 让其阻塞
	if(ret < 0){
		perror("sem_init");
		return -1;
	}
	ret = sem_init(&sem3,0,0); //初始化信号3 让其阻塞
	if(ret < 0){
		perror("sem_init");
		return -1;
	}
	ret = pthread_create(&tid1,NULL,fun1,NULL);//创建线程1
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	ret = pthread_create(&tid2,NULL,fun2,NULL);//创建线程2
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	ret = pthread_create(&tid3,NULL,fun3,NULL);//创建线程3
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	/*回收线程资源*/
	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
	pthread_join(tid3,NULL);

	/*销毁信号量*/
	sem_destroy(&sem1);
	sem_destroy(&sem2);
	sem_destroy(&sem3);

	return 0;
}
运行结果:

        该例程加入了信号量,使得线程的执行顺序变为可控的。在初始化信号量时,将信号量 1 填入资源,第一个线程调用 sem_wait 函数可以成功获得信号量,在执行完逻辑后使用 sem_pos 函数来释放。当执行函数 sem_wait 后,会执行 sem 自减操作,使下一次竞争被阻塞,直至通过 sem_pos 被释放。
        上述例程因 38 行初始化信号量 1 时候,使其默认获取到资源;第 43 48 行初始化信号量 2 3 时候,使之没有资源。于是在线程处理函数中,每个线程通过 sem_wait 函数来等待资源,发生阻塞。因信号量 1 初始值为有资源,故可以先执行线程 1 的逻辑。待执行完第 12 sem_wait 函数,会导致 sem1-1 ,使得下一次此线程会被阻塞。继而执行至 14 行,通过 sem_post 函数使 sem2 信号量获取资源,从而冲破阻塞执行线程 2 的逻辑 ... 以此类推完成线程的有序控制。

2.5 条件变量

        条件变量时一种同步机制,用来通知其他线程条件满足了。一般是用来通知对方共享数据的状态信息,因此条件变量时结合互斥量来使用的。

2.5.1 创建和销毁条件变量

  函数原型如下:

#include <pthread.h>
// 初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);//cond_at
tr 通常为 NULL
// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
这些函数成功时都返回 0

2.5.2 等待条件变量

函数原型如下:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
这需要结合互斥量一起使用,示例代码如下:
pthread_mutex_lock(&g_tMutex);
// 如果条件不满足则,会 unlock g_tMutex 
// 条件满足后被唤醒,会 lock g_tMutex
pthread_cond_wait(&g_tConVar, &g_tMutex);
/* 操作临界资源 */
pthread_mutex_unlock(&g_tMutex);

2.5.3 通知条件变量

函数原型如下:
int pthread_cond_signal(pthread_cond_t *cond);
        pthread_cond_signal 函数只会唤醒一个等待 cond 条件变量的线程,示例代码如下:
pthread_cond_signal(&g_tConVar);

2.5.4 程序示例

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>

static char g_buf[1000];
static pthread_mutex_t g_tMutex  = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t  g_tConVar = PTHREAD_COND_INITIALIZER;

static void *my_thread_func (void *data)
{
	while (1)
	{
		//sleep(1);
		/* 等待通知 */
		//while (g_hasData == 0);
		pthread_mutex_lock(&g_tMutex);
		pthread_cond_wait(&g_tConVar, &g_tMutex);	

		/* 打印 */
		printf("recv: %s\n", g_buf);
		pthread_mutex_unlock(&g_tMutex);
	}

	return NULL;
}


int main(int argc, char **argv)
{
	pthread_t tid;
	int ret;
	char buf[1000];
	
	/* 1. 创建"接收线程" */
	ret = pthread_create(&tid, NULL, my_thread_func, NULL);
	if (ret)
	{
		printf("pthread_create err!\n");
		return -1;
	}


	/* 2. 主线程读取标准输入, 发给"接收线程" */
	while (1)
	{
		fgets(buf, 1000, stdin);
		pthread_mutex_lock(&g_tMutex);
		memcpy(g_buf, buf, 1000);
		pthread_cond_signal(&g_tConVar); /* 通知接收线程 */
		pthread_mutex_unlock(&g_tMutex);
	}
	return 0;
}

3 总结

3.1 线程使用流程图

        有关多线程的创建流程如图 9.14 所示,首先需要创建线程,一旦线程创建完成后,线程与线程之间会发生竞争执行,抢占时间片来执行线程逻辑。在创建线程时候,可以通过创建线程的第四个参数传入参数,在线程退出时亦可传出参数被线程回收函数所回收,获取到传出的参数。

3.2 互斥量使用流程图

        当多个线程出现后,会遇到同时操作临界公共资源的问题,当线程操作公共资源时需要对线程进行保护加锁,防止其与线程在此线程更改变量时同时更改变量,待逻辑执行完毕后再次解锁,使其余线程再度开始竞争。互斥锁创建流程下图所示。

3.3 信号量使用流程图

        当多个线程出现后,同时会遇到无序执行的问题。有时候需要对线程的执行顺序做出限定,变引入了信号量,通过 PV 操作来控制线程的执行顺序,如下图所示。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

中间件安全:Apache 目录穿透.(CVE-2021-41773)

中间件安全&#xff1a;Apache 目录穿透.&#xff08;CVE-2021-41773&#xff09; Apache 的 2.4.49、2.4.50 版本 对路径规范化所做的更改中存在一个路径穿越漏洞&#xff0c;攻击者可利用该漏洞读取到Web目录外的其他文件&#xff0c;如系统配置文件、网站源码等&#xff0c…

Polygon zkEVM协议治理、升级及其流程

1. 引言 随着Polygon社区开发者和内部团队的测试深入&#xff0c;当前版本的Polygon zkEVM不可避免地需更新和某些升级。 为激励开发者对Polygon zkEVM做battle-test&#xff0c;已启动了bug-bounty&#xff1a; Rewards by Threat Level 由于zk-Rollup生态系统还处于萌芽阶…

算法设计与分析复习--贪心(二)

文章目录 上一篇哈夫曼编码单源最短路最小生成树Kruskal算法Prim算法 多机调度问题下一篇 上一篇 算法设计与分析复习–贪心&#xff08;一&#xff09; 哈夫曼编码 产生这种前缀码的方式称为哈夫曼树 哈夫曼树相关习题AcWing 148. 合并果子 #include <iostream> #inc…

LDO线性稳压器要不要并联二极管?

昨天介绍过了LDO是什么东西&#xff0c;那么对于它的应用场景是怎么的呢&#xff1f;LDO要不要并联二极管呢&#xff1f; 一般来说&#xff0c;LDO是不需要并联二极管的。 看下图第一个是典型电路&#xff0c;第二个是带可调节电压功能的LDO典型电路&#xff0c;从图里就可以…

设计模式-组合模式-笔记

“数据结构”模式 常常有一些组件在内部具有特定的数据结构&#xff0c;如果让客户程序依赖这些特定数据结构&#xff0c;将极大地破坏组件的复用。这时候&#xff0c;将这些特定数据结构封装在内部&#xff0c;在外部提供统一的接口&#xff0c;来实现与特定数据结构无关的访…

一起Talk Android吧(第五百五十四回:分享一个Retorfit使用错误的案例)

文章目录 1. 案例场景2. 案例现象3. 原因分析和解决方案3.1 原因分析3.2 解决方案4. 经验总结各位看官们大家好,上一回中咱们说的例子是"解析Retrofit返回的数据",本章回中将分享一个 Retrofit使用错误的案例。闲话休提,言归正转,让我们一起Talk Android吧! 1. …

三层交换机实现不同VLAN间通讯

默认时&#xff0c;同一个VLAN中的主机才能彼此通信&#xff0c;那么交换机上的VLAN用户之间如何通信&#xff1f; 要实现VLAN之间用户的通信&#xff0c;就必须借助路由器或三层交换机来完成。 下面以三层交换机为例子说明&#xff1a; 注意&#xff1a; 1.交换机与三层交换…

HWS-CTF-第七期山大站-inverse

文章目录 inversemainworkread_intread_n 思路onegadget exp 第一次真正意义上独立在比赛中做出题目来了&#xff0c;距离真正意义接触CTF-PWN差不多正好两个月。但由于不知道靶场要自己开而且端口每次自己打开会改&#xff0c;交flag稍微晚了些&#xff08;我太菜了&#xff0…

Java中锁的深入理解

目录 对象头的理解 Monitor&#xff08;锁&#xff09; 锁类型 偏向锁 偏向锁的优化机制 轻量级锁 重量级锁 对象头的理解 在32位Java虚拟机中普通对象的对象头是占用8个字节&#xff0c;其中4个字节为Mark Word。用来存储对象的哈希值&#xff0c;对象创建后在JVM中的…

【顺序表的实现】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 1. 数据结构相关概念 1、什么是数据结构 2、为什么需要数据结构&#xff1f; 2、顺序表 1、顺序表的概念及结构 1.1 线性表 2、顺序表分类 3、动态顺序表的实现 总…

ssm+vue的高校疫情防控管理系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的高校疫情防控管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结…

【C++入门】拷贝构造运算符重载

目录 1. 拷贝构造函数 1.1 概念 1.2 特征 1.3 常用场景 2. 赋值运算符重载 2.1 运算符重载 2.2 特征 2.3 赋值运算符 前言 拷贝构造和运算符重载是面向对象编程中至关重要的部分&#xff0c;它们C编程中的一个核心领域&#xff0c;本期我详细的介绍拷贝构造和运算符重载。 1. …

面向对象与面向过程的区别

面向对象 以对象为中心&#xff0c;把数据封装成为一个整体&#xff0c;其他数据无法直接修改它的数据&#xff0c;将问题分解成不同对象&#xff0c;然后给予对象相应的属性和行为。 面向过程 关注代码过程&#xff0c;直接一程序来处理数据&#xff0c;各模块之间有调用与…

OSI参考模型

目录 一. OSI参考模型的各层功能二. 网络排错三. 网络安全四. 实体、协议、服务和服务访问点SAP五. TCP IP体系结构 一. OSI参考模型的各层功能 \quad \quad \quad \quad 我们首先来看应用层实现的功能 每个字段的各种取值所代表的意思 \quad \quad 比如要保存的文件内容是ab…

OpenAI 董事会与 Sam Altman 讨论重返 CEO 岗位事宜

The Verge 援引多位知情人士消息称&#xff0c;OpenAI 董事会正在与 Sam Altman 讨论他重新担任首席执行官的可能性。 有一位知情人士表示&#xff0c;Altman 对于回归公司一事的态度暧昧&#xff0c;尤其是在他没有任何提前通知的情况下被解雇后。他希望对公司的治理模式进行重…

【开源】基于Vue.js的高校实验室管理系统的设计和实现

项目编号&#xff1a; S 015 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S015&#xff0c;文末获取源码。} 项目编号&#xff1a;S015&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容2.1 实验室类型模块2.2 实验室模块2.3 实…

Tomcat无法映射到activiti-app导致activiti无法启动页面

原因之一&#xff1a;JDK版本与Tomcat版本不匹配&#xff0c;jdk8 yyds 我使用的是JDK11&#xff0c;Tomcat是9.0的&#xff0c;都是最新的&#xff0c;但还是不行&#xff0c;最后JDK改为8&#xff0c;tomcat的cmd后台没有报错&#xff0c;activiti-pp也可以正常访问了,很神奇…

鸿蒙应用开发之打包与上架

一、概述 当您开发、调试完HarmonyOS应用/元服务&#xff0c;就可以前往AppGallery Connect申请上架&#xff0c;华为审核通过后&#xff0c;用户即可在华为应用市场获取您的HarmonyOS应用/元服务。 HarmonyOS会通过数字证书与Profile文件等签名信息来保证应用的完整性&#…

数电实验-----实现74LS139芯片扩展为3-8译码器以及应用(Quartus II )

目录 一、74LS139芯片介绍 芯片管脚 芯片功能表 二、2-4译码器扩展为3-8译码器 1.扩展原理 2.电路图连接 3.仿真结果 三、3-8译码器的应用&#xff08;基于74ls139芯片&#xff09; 1.三变量表决器 2.奇偶校验电路 一、74LS139芯片介绍 74LS139芯片是属于2-4译码器…

Halcon Solution Guide I basics(0): 导论解析

文章目录 文章专栏前言文章目录翻译文档的说明 结论LOL比赛结局 文章专栏 Halcon开发 前言 今天开始看Halcon的官方文档。由于市面上的教学主要是以基础的语法&#xff0c;算子简单介绍为主。所以我还是得看官方的文本。别的不多说了。有道词英语词典&#xff0c;启动。 还有…