Linux 多任务编程之线程

这里写目录标题

在这里插入图片描述

1、线程概述

在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行 什么,只是维护应用程序所需的各种资源,而线程则是真正的执行实体。 所以, 线程是轻量级的进程(LWP:lightweightprocess),在Linux环境下线程的本质 仍是进程。 为了让进程完成一定的工作,进程必须至少包含一个线程。

进程是系统分配资源的基本单位。

线程是CPU调度的基本单位。

线程是轻量级的进程(LWP:light weight process)

在这里插入图片描述
进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源,所以我们也说,进程是系统分配资源的最小单位。 线程存在于进程当中(进程可以认为是线程的容器),是CPU调度执行的最小单位。说通俗点,线程就是干活的。 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。 线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。 如果说进程是一个资源管家,负责从主人那里要资源的话,那么线程就是干活的苦力。一个管家必须完成一项工作,就需要最少一个苦力,也就是说,一个进程最少包含一个线程,也可以包含多个线程。苦力要干活,就需要依托于管家,所以说一个线程,必须属于某一个进程。 进程有自己的地址空间,线程使用进程的地址空间,也就是说,进程里的资源,线程都是有权访问的,比如说堆啊,栈啊,静态存储区什么的。
进程是操作系统分配资源的最小单位
线程是操作系统调度的最小单位

线程的特点:

  1. 线程是轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
  2. 从内核里看进程和线程是一样的,都有各自不同的PCB.
  3. 进程可以蜕变成线程
  4. 在linux下,线程最是小的执行单位;进程是最小的分配资源单位

查看指定进程的LWP号: ps -Lf pid 实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数 clone 。 Ø 如果复制对方的地址空间,那么就产出一个“进程”; Ø 如果共享对方的地址空间,就产生一个“线程”。 Linux内核是不区分进程和线程的, 只在用户层面上进行区分。所以,线程所有操作函数 pthread* 是库函数,而非系统调用。

线程共享资源

  1. 文件描述符表
  2. 每种信号的处理方式
  3. 当前工作目录
  4. 用户ID和组ID 内存地址空间 (.text/.data/.bss/heap/共享库)

线程非共享资源

  1. 线程id
  2. 处理器现场和栈指针(内核栈)
  3. 独立的栈空间(用户空间栈)
  4. errno变量
  5. 信号屏蔽字
  6. 调度优先级

线程的优缺点

优点: Ø 提高程序并发性 Ø 开销小 Ø 数据通信、共享数据方便
缺点: Ø 库函数,不稳定 Ø 调试、编写困难、gdb不支持 Ø 对信号支持不好 优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。

2、线程号

进程号用 pid_t 数据类型表示,是一个非负整数。线程号则用 pthread_t 数据类型来表示,Linux 使用无符号长整数表示。 有的系统在实现pthread_t 的时候,用一个结构体来表示,所以在可移植的操作系统实现不能把它做为整数处理。

获取线程号pthread_self函数:

#include <pthread.h>
pthread_t pthread_self(void);
功能:
    获取线程号。
参数:
    无
返回值:
    调用线程的线程 ID

在这里插入图片描述
gcc xxx.c -o a.out -pthread

1、线程的创建

#include <pthread.h>
int pthread_create(pthread_t *thread,
            const pthread_attr_t *attr,
            void *(*start_routine)(void *),
            void *arg );
功能:
    创建一个线程
参数:
    thread:线程标识符地址。
    attr:线程属性结构体地址,通常设置为 NULL。
    start_routine:线程函数的入口地址。
    arg:传给线程函数的参数。

返回值:
    成功:0
    失败:非 0

在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针startroutine决定

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
void *pthread_func01(void *arg)
{
	//线程的核心代码
	while(1)
	{
		printf("%s\n",(char *)arg);
		sleep(1);
	}
}
int main()
{
	//创建一个线程ID
	pthread_t tid1;
	//创建一个线程
	pthread_create(&tid1,NULL, pthread_func01 ,"第一个线程");
	
	//创建一个线程ID
	pthread_t tid2;
	//创建一个线程
	pthread_create(&tid2,NULL, pthread_func01 ,"第二个线程");
	while(1);
	return 0;
}

运行结果:
在这里插入图片描述

2、回收线程资源

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能:
    等待线程结束(此函数会阻塞),并回收线程资源,类似进程的 wait() 函数。
    如果线程已经结束,那么该函数会立即返回。
参数:
    thread:被等待的线程号。
    retval:用来存储线程退出状态的指针的地址
返回值:
    成功:0
    失败:非 0

案例:

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
void *pthread_func01(void *arg)
{
	//线程的核心代码
	int i=0;
	for(i=0;i<5;i++)
	{
		printf("%s--->i=%d\n",(char *)arg,i);
		sleep(1);
	}
	
	return NULL;
}
int main()
{
	//创建一个线程ID
	pthread_t tid1;
	//创建一个线程
	pthread_create(&tid1,NULL, pthread_func01 ,"第一个线程");
	
	//线程等待pthread_join
	printf("等待线程\n");
	pthread_join(tid1, NULL);//阻塞 直到线程结束
	printf("线程已经退出\n");
	return 0;
}

运行结果:
在这里插入图片描述
案例:获取线程的返回值(了解)

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
void *pthread_func01(void *arg)
{
	//线程的核心代码
	int i=0;
	for(i=0;i<5;i++)
	{
		printf("%s--->i=%d\n",(char *)arg,i);
		sleep(1);
	}
	
	return (void *)5;
}
int main()
{
	//创建一个线程ID
	pthread_t tid1;
	//创建一个线程
	pthread_create(&tid1,NULL, pthread_func01 ,"第一个线程");
	
	//线程等待pthread_join
	int ret;
	pthread_join(tid1, (void **)&ret);//阻塞 直到线程结束
	printf("ret = %d\n", ret);
	
	return 0;
}

运行结果:
在这里插入图片描述

3、线程分离pthread_detach 不阻塞

int pthread_detach(pthread_t thread);
功能:
    使调用线程与当前进程分离,分离后不代表此线程不依赖与当前进程,线程分离的目的是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。所以,此函数不会阻塞
参数:
    thread:线程号。
返回值:
    成功:0
    失败:非0

案例:

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
void *pthread_func01(void *arg)
{
	//线程的核心代码
	int i=0;
	for(i=0;i<5;i++)
	{
		printf("%s--->i=%d\n",(char *)arg,i);
		sleep(1);
	}
	
	return NULL;
}
int main()
{
	int i=0;
	for(i=0;i<5;i++)
	{
		char buf[128]="";
		sprintf(buf,"第%d个线程",i+1);
		//创建一个线程ID
		pthread_t tid1;
		//创建一个线程
		pthread_create(&tid1,NULL, pthread_func01 ,buf);
		
		//线程等待pthread_detach
		pthread_detach(tid1);//不阻塞
	}
	
	getchar();
	
	return 0;
}

运行结果:
在这里插入图片描述
为啥全是“第5个线程” pthread_create不阻塞 只是建立关系 啥时候调用线程函数是cpu决定

案例:创建多线程

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
void *pthread_func01(void *arg)
{
	//线程的核心代码
	int i=0;
	for(i=0;i<5;i++)
	{
		printf("%u--->i=%d\n",pthread_self(),i);
		sleep(1);
	}
	
	return NULL;
}
int main()
{
	int i=0;
	for(i=0;i<5;i++)
	{
		//创建一个线程ID
		pthread_t tid;
		//创建一个线程
		pthread_create(&tid,NULL, pthread_func01 ,NULL);
		
		//线程等待pthread_detach
		pthread_detach(tid);//不阻塞
	}
	
	getchar();
	
	return 0;
}

3、线程的退出

进程:
main函数中return结束 (自然死亡)
exit或_exit结束 (自杀)
kill 结束(他杀)
线程:
线程函数return (自然死亡)
pthread_exit (自杀)
pthread_cancel (他杀)

3.1、pthread_exit函数(自杀)

#include <pthread.h>
void pthread_exit(void *retval);
功能:
    退出调用线程。一个进程中的多个线程是共享该进程的数据段,
    因此,通常线程退出后所占用的资源并不会释放。
参数:
    retval:存储线程退出状态的指针。
返回值:无

案例:

#include<stdio.h>
#include<pthread.h>
void* deal_fun(void *arg)
{
	int i=0;
	for(i=0;i<10;i++)
	{
		printf("线程运行中i = %d\n",i);
		if(i==5)
			pthread_exit(NULL);//结束线程 自杀
		sleep(1);
	}
	return NULL;
}
int main()
{

	pthread_t tid;
	pthread_create(&tid, NULL, deal_fun , NULL);
	pthread_join(tid,NULL);
	return 0;
}

运行结果:
在这里插入图片描述

3.2、pthread_cancel(线程的取消)

#include <pthread.h>
int pthread_cancel(pthread_t thread);
功能:
    杀死(取消)线程
参数:
    thread : 目标线程ID。
返回值:
    成功:0
    失败:出错编号
注意:线程的取消并不是实时的,而又一定的延时。
需要等待线程到达某个取消点(检查点)

注意:线程的取消并不是实时的,而又一定的延时。需要等待线程到达某个取消点 (检查点)。 类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓 库、城里等)才能存储进度。 杀死线程也不是立刻就能完成,必须要到达取消点。 取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统 调用creat,open,pause,close,read,write…执行命令man7pthreads可以 查看具备这些取消点的系统调用列表。 可粗略认为一个系统调用(进入内核)即为 一个取消点。

案例:

#include<stdio.h>
#include<pthread.h>
void* deal_fun(void *arg)
{
	int i=0;
	for(i=0;i<10;i++)
	{
		printf("线程运行中i = %d\n",i);
		sleep(1);//取消点 
	}
	return NULL;
}
int main()
{

	pthread_t tid;
	pthread_create(&tid, NULL, deal_fun , NULL);
	
	//3秒过后 取消tid线程
	sleep(3);
	pthread_cancel(tid);
	
	pthread_join(tid,NULL);
	return 0;
}

运行结果:
在这里插入图片描述

3.2.1线程取消分析

在这里插入图片描述

线程的取消状态:能不能取消

是否接受其他线程的取消信号 pthread_setcancelstate 设置取消状态

int pthread_setcancelstate(int state, int *oldstate);
//PTHREAD_CANCEL_ENABLE 可以取消
// PTHREAD_CANCEL_DISABLE 不允许取消

线程的取消点:立即取消 还是 到达取消点取消

线程是否立即取消(默认遇到取消点 才取消)pthread_setcanceltype 设置取消类型

int pthread_setcanceltype(int type, int *oldtype);
//PTHREAD_CANCEL_DEFERRED 取消点取消
//PTHREAD_CANCEL_ASYNCHRONOUS 立即取消

案例:

#include<stdio.h>
#include<pthread.h>
void* deal_fun(void *arg)
{
	//设置取消状态(能不能取消)
	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL);//能取消
	//设置取消类型(在何时取消?)
	pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL);//立即取消
	
	int i=0;
	for(i=0;i<10;i++)
	{
		printf("线程运行中i = %d\n",i);
		sleep(1);
	}
	return NULL;
}
int main()
{

	pthread_t tid;
	pthread_create(&tid, NULL, deal_fun , NULL);
	
	//3秒过后 取消tid线程
	sleep(3);
	pthread_cancel(tid);
	
	pthread_join(tid,NULL);
	return 0;
}

运行结果:
在这里插入图片描述

4、注册线程清理函数

线程和进程一样,线程也可以注册它退出时候 调用的函数,这样的函数 就叫线程清理函数

4.1、注册清理函数:pthread_cleanup_push

 void pthread_cleanup_push(void (*routine)(void *),void *arg);
 //功能:将清理函数压栈,注册清理函数
 //参数:
     //routine:线程清理函数
     //arg:传递给清理函数的参数

4.2、弹出清理函数:pthread_cleanup_pop

void pthread_cleanup_pop(int execute);
//功能:清理函数弹栈 (执行)
//参数:
    //execute 线程清理函数执行标志位
    非0:弹出清理函数  执行清理函数
    0:弹出清理函数 不执行清理函数

4.3、当线程执行以下情况会调用清理函数

1、调用pthread_exit 退出线程  执行清理函数
2、pthread_cancel  响应其他线程的取消请求 执行清理函数
3、用非0 的方式调用pthread_cleanup_pop 执行清理函数

注意:不管pthread_cleanup_pop 能否被执行 必须成对出现

案例:

#include<stdio.h>
#include<pthread.h>
#include<string.h>
#include<stdlib.h>
void clean_fun(void *arg)//arg = str
{
	char *str = (char *)arg;
	if(str != NULL)
	{
		printf("堆区空间释放了2\n");
		free(str);
		str =NULL;
	}
	return;
}
void* deal_fun(void *arg)
{
	//从堆区申请空间
	char *str=(char *)calloc(1,32);
	strcpy(str, "hello pthread");
	
	//注册清理函数
	pthread_cleanup_push(clean_fun, str);
	printf("%s\n", str);
	sleep(2);
	pthread_exit(NULL);
	
	//释放空间
	if(str != NULL)
	{
		printf("堆区空间释放了1\n");
		free(str);
		str =NULL;
	}
	//弹出清理函数(push pop一定要成对出现)
	pthread_cleanup_pop(1);
	
	return NULL;
}
int main()
{

	pthread_t tid;
	pthread_create(&tid, NULL, deal_fun , NULL);	
	pthread_join(tid,NULL);
	return 0;
}

运行结果:
在这里插入图片描述

5、线程的属性

线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。
可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数

typedef struct
{
    int  etachstate; //线程的分离状态
    int  schedpolicy; //线程调度策略
    struct sched_param schedparam; //线程的调度参数
    int  inheritsched; //线程的继承性
    int  scope; //线程的作用域
    size_t  guardsize; //线程栈末尾的警戒缓冲区大小
    int stackaddr_set; //线程的栈设置
    void* stackaddr; //线程栈的位置
    size_t  stacksize; //线程栈的大小
} pthread_attr_t;

主要结构体成员:

1. 线程分离状态  
2. 线程栈大小(默认平均分配)  
3. 线程栈警戒缓冲区大小(位于栈末尾)  
4. 线程栈最低地址 属性值不能直接设置,须使用相关函数进行操作,
初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。 
线程属性主要包括如下属性:
	作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、
	分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。
默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级.
线程属性初始化
注意:应先初始化线程属性,再pthread_create创建线程 
初始化线程属性函数: 
	int pthread_attr_init(pthread_attr_t *attr);  
函数返回值:
	成功:0;失败:错误号 
销毁线程属性所占用的资源函数: int pthread_attr_destroy(pthread_attr_t *attr);   函数返回值:
	成功:0;失败:错误号 
线程分离状态的函数: 设置线程属性,分离or非分离
 int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);  
获取程属性,分离or非分离
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstat);  
参数:
    attr:已初始化的线程属性
detachstate:
    分离状态 u PTHREAD_CREATE_DETACHED(分离线程) u
    PTHREAD_CREATE_JOINABLE(非分离线程)

案例:通过线程属性 设置 线程分离

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

void* deal_fun(void *arg)
{
	int i=0;
	for(i=0;i<3;i++)
	{
		printf("线程执行中i=%d\n", i);
		sleep(1);
	}
	return NULL;
}
int main()
{
	//通过设置线程属性 完成线程分离
	pthread_attr_t attr;
	//初始化线程属性
	pthread_attr_init(&attr);
	//设置线程属性 分离
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
	
	//pthread_create创建线程 其实仅仅是建立关系 不会等待线程执行 
	//但是如果线程函数执行时间特点短 还没有来得及执行pthread_detach 线程就已经结束
	pthread_t tid;
	pthread_create(&tid, &attr, deal_fun , NULL);
	//pthread_detach(tid);//线程分离 
	
	getchar();
	
	//释放线程属性
	pthread_attr_destroy(&attr);
	
	return 0;
}

运行结果:
在这里插入图片描述

6、线程间的同步互斥

1、同步互斥的概述

互斥:是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源
总结:一个公共资源同一时刻只能被一个进程或线程使用。(无执行顺序)
同步:一个公共资源同一时刻只能被一个进程或线程使用。(规定的了执行顺序)
同步是有序的互斥。

案例:

#include<stdio.h>
#include<pthread.h>
#include<string.h>
#include<stdlib.h>
void myPrintf(char *str)
{
	while(*str != 0)
	{
		printf("%c",*str);
		fflush(stdout);
		str++;
		sleep(1);
	}
}
void* deal_fun(void *arg)
{
	myPrintf(arg);
	
	return NULL;
}
int main()
{
	pthread_t tid1;
	pthread_t tid2;
	
	pthread_create(&tid1, NULL, deal_fun, "hello");
	pthread_create(&tid2, NULL, deal_fun, "world");
	
	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
	return 0;
}

运行结果:
在这里插入图片描述

2、互斥锁

互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即加锁( lock )和解锁( unlock )

互斥:只需要一把锁

在这里插入图片描述
互斥锁的操作流程如下:
1)在访问共享资源临界区域前,对互斥锁进行加锁。
2)在访问完成后释放互斥锁上的锁。
3)对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。

2.1、初始化一把锁pthread_mutex_init

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex,
    const pthread_mutexattr_t *attr);
功能:
    初始化一个互斥锁。
参数:
    mutex:互斥锁地址。类型是 pthread_mutex_t 。
    attr:设置互斥量的属性,通常可采用默认属性,即可将 attr 设为 NULL。。
返回值:
    成功:0,成功申请的锁默认是打开的。
    失败:非 0 错误码
    
静态锁的创建:
可以使用宏 PTHREAD_MUTEX_INITIALIZER 静态初始化互斥锁,
比如:pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;
这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_mutex_init() 
来完成动态初始化,不同之处在于 PTHREAD_MUTEX_INITIALIZER 宏不进行错误检查 

2.2、销毁互斥锁

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:
    销毁指定的一个互斥锁。互斥锁在使用完毕后,必须要对互斥锁进行销毁,以释放资源。
参数:
    mutex:互斥锁地址。
返回值:
    成功:0
    失败:非 0 错误码

2.3、申请上锁

申请上锁
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:
    对互斥锁上锁,若互斥锁已经上锁,则调用者阻塞,直到互斥锁解锁后再上锁。
参数:
    mutex:互斥锁地址。
返回值:
    成功:0
    失败:非 0 错误码
int pthread_mutex_trylock(pthread_mutex_t *mutex);
   调用该函数时,若互斥锁未加锁,则上锁,返回 0;
   若互斥锁已加锁,则函数直接返回失败,即 EBUSY

2.4、解锁

#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:
    对指定的互斥锁解锁。
参数:
    mutex:互斥锁地址。
返回值:
    成功:0
    失败:非0错误码

案例:

#include<stdio.h>
#include<pthread.h>
#include<string.h>
#include<stdlib.h>
//定义一把锁
pthread_mutex_t mutex;

void myPrintf(char *str)
{
	while(*str != 0)
	{
		printf("%c",*str);
		fflush(stdout);
		str++;
		sleep(1);
	}
}
void* deal_fun01(void *arg)
{
	//上锁 pthread_mutex_lock
	pthread_mutex_lock(&mutex);
	
	myPrintf(arg);
	
	//解锁 pthread_mutex_unlock
	pthread_mutex_unlock(&mutex);
	return NULL;
}
void* deal_fun02(void *arg)
{
	//上锁 pthread_mutex_lock
	pthread_mutex_lock(&mutex);
	
	myPrintf(arg);
	
	//解锁 pthread_mutex_unlock
	pthread_mutex_unlock(&mutex);
	return NULL;
}
int main()
{
	//初始化锁
	pthread_mutex_init(&mutex,NULL);
	
	pthread_t tid1;
	pthread_t tid2;
	
	pthread_create(&tid1, NULL, deal_fun01, "hello");
	pthread_create(&tid2, NULL, deal_fun02, "world");
	
	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
    //释放锁的资源
    pthread_mutex_destroy(&mutex);
	return 0;
}

运行结果:
在这里插入图片描述

3、死锁

互斥条件 某资源只能被一个进程使用,其他进程请求该资源时,只能等待,直到 资源使用完毕后释放资源。 请求和保持条件 程序已经保持了至少一个资源,但是 又提出了新要求,而这个资源被其他进程占用,自己占用资源却保持不放。 不可 抢占条件 进程已获得的资源没有使用完,不能被抢占。 循环等待条件 必然存在 一个循环链。4)处理死锁的思路 预防死锁 破坏死锁的四个必要条件中的一个 或多个来预防死锁。 避免死锁 和预防死锁的区别就是,在资源动态分配过程中, 用某种方式防止系统进入不安全的状态。 检测死锁 运行时出现死锁,能及时发现 死锁,把程序解脱出来 解除死锁 发生死锁后,解脱进程,通常撤销进程,回收资 源,再分配给正处于阻塞状态的进程。5)预防死锁的方法 破坏请求和保持条件 协议1: 所有进程开始前,必须一次性地申请所需的所有资源,这样运行期间就 不会再提出资源要求,破坏了请求条件,即使有一种资源不能满足需求,也不会给 它分配正在空闲的资源,这样它就没有资源,就破坏了保持条件,从而预防死锁的 发生。 协议2: 允许一个进程只获得初期的资源就开始运行,然后再把运行完的 资源释放出来。然后再请求新的资源。 破坏不可抢占条件 当一个已经保持了某种 不可抢占资源的进程,提出新资源请求不能被满足时,它必须释放已经保持的所有 资源,以后需要时再重新申请。 破坏循环等待条件 对系统中的所有资源类型进行 线性排序,然后规定每个进程必须按序列号递增的顺序请求资源。假如进程请求到 了一些序列号较高的资源,然后有请求一个序列较低的资源时,必须先释放相同和 更高序号的资源后才能申请低序号的资源。多个同类资源必须一起请求。
在这里插入图片描述

4、读写锁

能够允许多个读出,只允许一个写入。
读不能写,写不能读;同一时刻,写只能一个线程,读可以多个线程

读写锁的特点:

1)如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
2)如果有其它线程写数据,则其它线程都不允许读、写操作

读写锁分为读锁和写锁,规则:

1)如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁。
2)如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁

4.1、初始化读写锁

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock,
    const pthread_rwlockattr_t *attr);
功能:
    用来初始化 rwlock 所指向的读写锁。
参数:
    rwlock:指向要初始化的读写锁指针。
    attr:读写锁的属性指针。
    如果 attr 为 NULL 则会使用默认的属性初始化读写锁,
    否则使用指定的 attr 初始化读写锁。。
返回值:
    成功:0,读写锁的状态将成为已初始化和已解锁。
    失败:非 0 错误码
    
可以使用宏 PTHREAD_RWLOCK_INITIALIZER 静态初始化读写锁,
比如:pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER;
这种方法等价于使用 NULL 
指定的 attr 参数调用 pthread_rwlock_init() 
来完成动态初始化,不同之处在于PTHREAD_RWLOCK_INITIALIZER 宏不进行错误检查

4.2、释放读写锁

#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:
    用于销毁一个读写锁,并释放所有相关联的资源
    (所谓的所有指的是由 pthread_rwlock_init() 自动申请的资源) 。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码

4.3、申请读锁

#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:
    以阻塞方式在读写锁上获取读锁(读锁定)。
    如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。
    如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。
    一个线程可以在一个读写锁上多次执行读锁定。
    线程可以成功调用 pthread_rwlock_rdlock() 函数 n 次,
    但是之后该线程必须调用 pthread_rwlock_unlock() 函数 n 次才能解除锁定。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
    用于尝试以非阻塞的方式来在读写锁上获取读锁。
 
    如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回

4.4、申请写锁

#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:
    在读写锁上获取写锁(写锁定)。
    如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁。
    如果调用线程未获取写锁,则它将阻塞直到它获取了该锁。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
  用于尝试以非阻塞的方式来在读写锁上获取写锁。

  如果有任何的读者或写者持有该锁,则立即失败返回

4.5、释放读写锁

#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
功能:
    无论是读锁或写锁,都可以通过此函数解锁。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码

案例:

#include<stdio.h>
#include<pthread.h>
#include<string.h>
#include<stdlib.h>
//定义一把读写锁
pthread_rwlock_t rwlock;
//模拟公共资源
int num = 0;


void* read_fun(void *arg)
{
	while(1)
	{
		//上读锁
		pthread_rwlock_rdlock(&rwlock);
		printf("%lu线程读取num=%d\n",pthread_self(), num);
		//解读锁
		pthread_rwlock_unlock(&rwlock);
		sleep(1);
	}
	
	return NULL;
}
void* write_fun(void *arg)
{
	while(1)
	{
		//上写锁
		pthread_rwlock_wrlock(&rwlock);
		num++;
		printf("%lu线程写入num=%d\n",pthread_self(), num);
		//解写锁
		pthread_rwlock_unlock(&rwlock);
		
		sleep(3);
	}
	return NULL;
}
int main()
{
	//初始化一把读写锁
	pthread_rwlock_init(&rwlock,NULL);
	pthread_t tid1_r;
	pthread_t tid2_r;
	pthread_t tid3_w;
	
	pthread_create(&tid1_r, NULL, read_fun, NULL);
	pthread_create(&tid2_r, NULL, read_fun, NULL);
	pthread_create(&tid3_w, NULL, write_fun, NULL);
	
	pthread_join(tid1_r,NULL);
	pthread_join(tid2_r,NULL);
	pthread_join(tid3_w,NULL);
	//销毁读写锁
	pthread_rwlock_destroy(&rwlock);
	return 0;
}

5、条件变量

与互斥锁不同,条件变量是用来等待而不是用来上锁的,条件变量本身不是锁! 条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。 条件变量的两个动作: 条件不满, 阻塞线程 当条件满足, 通知阻塞的线程开始工作 条件变量的类型: pthread_cond_t
在这里插入图片描述

5.1、条件变量初始化pthread_cond_init

#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond,
    const pthread_condattr_t *attr);
功能:
    初始化一个条件变量
参数:
    cond:指向要初始化的条件变量指针。
    attr:条件变量属性,通常为默认值,传NULL即
返回值:
    成功:0
    失败:非0错误号
 也可以使用静态初始化的方法,初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

5.2、释放条件变量pthread_cond_destroy

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
功能:
    销毁一个条件变量
参数:
    cond:指向要初始化的条件变量指针
返回值:
    成功:0
    失败:非0错误号

5.3、等待条件pthread_cond_wait

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond,
    pthread_mutex_t *mutex);
功能:
    阻塞等待一个条件变量
    a) 阻塞等待条件变量cond(参1)满足
    b) 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
            a) b) 两步为一个原子操作。
    c) 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);
参数:
    cond:指向要初始化的条件变量指针
    mutex:互斥锁
返回值:
    成功:0
    失败:非0错误号
    
int pthread_cond_timedwait(pthread_cond_t *cond,
    pthread_mutex_t *mutex,
    const struct *abstime);
功能:
    限时等待一个条件变量
参数:
    cond:指向要初始化的条件变量指针
    mutex:互斥锁
    abstime:绝对时间
返回值:
    成功:0
    失败:非0错误号

5.4、唤醒等待在条件变量上的线程

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
功能:
    唤醒至少一个阻塞在条件变量上的线程
参数
    cond:指向要初始化的条件变量指
返回值
    成功:0
    失败:非0错误号
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:
    唤醒全部阻塞在条件变量上的线程
参数:
    cond:指向要初始化的条件变量指针
返回值:
    成功:0
    失败:非0错误号
int pthreadcondbroadcast(pthreadcondt *cond); 
功能: 给阻塞在条件变量上的所有线程发送信号 参数: cond 条件变量的地址

案例:生产者、消费者

在这里插入图片描述

#include<stdio.h>
#include<pthread.h>
#include<string.h>
#include<stdlib.h>
#include<time.h>
//定义一个互斥锁
pthread_mutex_t mutex;
//定义一个条件变量
pthread_cond_t cond;
//定义一个仓库num
int num = 0;
void* producer(void *arg)
{
	//不停的生产
	while(1)
	{
		//模拟一个生产时间
		sleep(rand()%3);//0~2秒
		
		//将数据放入仓库(num++)
		pthread_mutex_lock(&mutex);
		num++;
		printf("%s 在生产 当前num=%d\n", (char *)arg,num);
		//通知其他消费线程 条件变量 满足
		pthread_cond_broadcast(&cond);
		//解锁
		pthread_mutex_unlock(&mutex);
	}
}

void* consumer(void *arg)
{
	while(1)
	{
		//模拟去仓库取产品
		//上锁
		pthread_mutex_lock(&mutex);
		//先判断num==0
		while(num == 0)//防止 上锁成功 结果数据num还是0
			pthread_cond_wait(&cond,&mutex);//阻塞->等待条件同时解锁->条件满足同时上锁
			
		num--;
		printf("%s消费了一个产品 num剩余数量num=%d\n",(char *)arg,num);
		//解锁
		pthread_mutex_unlock(&mutex);
		
		//模拟一个消费时间
		sleep(rand()%3);
	}
}
int main()
{
	//设置随机数种子
	srand(time(NULL));
	
	//初始化互斥锁
	pthread_mutex_init(&mutex,NULL);
	//初始化条件变量
	pthread_cond_init(&cond,NULL);
	
	//创建三个线程:两个消费者  一个生产者
	pthread_t tid1,tid2,tid3;
	pthread_create(&tid1, NULL, producer,"生产者");
	pthread_create(&tid2, NULL, consumer,"消费者A");
	pthread_create(&tid3, NULL, consumer,"消费者B");
	
	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
	pthread_join(tid3,NULL);
	
	//销毁互斥锁
	pthread_mutex_destroy(&mutex);
	//销毁条件变量
	pthread_cond_destroy(&cond);
	return 0;
}

运行结果:
在这里插入图片描述

6、信号量

信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。
当信号量值大于 0 时,则可以访问,否则将阻塞。
PV 原语是对信号量的操作,一次 P 操作使信号量减1(如果信号量为0不允许再减,阻塞),一次 V 操作使信号量加1。
信号量数据类型为:sem_t

信号量用于互斥:

如果是互斥,不管有多少个线程(进程),只需要一个信号量并初始化1,每个线程(进程)p—>具体任务—>v
在这里插入图片描述

信号量用于同步:

如果是同步:有几个线程(进程),就要有几个信号量,将需要先运行的线程(进程)的信号量初始化为1,其他初始化0。每个线程(进程)都是先P自己的信号量,然后再V下一个线程(进程)的信号量。
在这里插入图片描述
在这里插入图片描述

6.1、初始化信号量:sem_init

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value)
功能:
    创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化。
参数:
    sem:信号量的地址
    pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。
    value:信号量的初始值
返回值:
    成功:0
    失败: - 1

6.2、P操作 信号量减一 :sem_wait

int sem_wait(sem_t *sem);
功能: 将信号量减一,如果信号量的值为0 则阻塞,大于0可以减一
 参数:信号量的地址
 返回值:成功返回0 失败返回-1
 
尝试对信号量减一
int sem_trywait(sem_t *sem);
功能: 尝试将信号量减一,如果信号量的值为0 不阻塞,立即返回 ,大于0可以减一
参数:信号量的地址
返回值:成功返回0 失败返回-1

6.3、V 操作 信号量加一:

int sem_post(sem_t *sem);
功能:将信号量加一
参数:信号量的地址
返回值:成功返回0 失败返回-1

6.4、销毁信号量:sem_destroy

int sem_destroy(sem_t *sem);
功能: 销毁信号量
参数: 信号量的地址
返回值:成功返回0 失败返回-1

案例1:信号量的互斥(线程)

#include<stdio.h>
#include<pthread.h>
#include<string.h>
#include<stdlib.h>
#include<semaphore.h>
//定义一个信号量
sem_t sem;

void myPrintf(char *str)
{
	while(*str != 0)
	{
		printf("%c", *str);
		fflush(stdout);
		str++;
		sleep(1);
	}
}
void* my_fun01(void *arg)
{	//P -1
	sem_wait(&sem);
	myPrintf(arg);
	//V +1
	sem_post(&sem);
}

void* my_fun02(void *arg)
{
	//P -1
	sem_wait(&sem);
	myPrintf(arg);
	//V +1
	sem_post(&sem);
}
int main()
{
	//初始化信号量 
	//第二参数0表示线程同步互斥,1表示进程同步互斥
	//第三个参数 是信号量的初始值
	sem_init(&sem, 0, 1);
	
	//创建两个线程
	pthread_t tid1,tid2;
	
	pthread_create(&tid1,NULL, my_fun01, "hello");
	pthread_create(&tid2,NULL, my_fun02, "world");
	
	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
	
	//信号量撤销
	sem_destroy(&sem);
	
	return 0;
}

运行结果:
在这里插入图片描述

案例2:信号量的同步(线程)

#include<stdio.h>
#include<pthread.h>
#include<string.h>
#include<stdlib.h>
#include<semaphore.h>
//定义两个信号量
sem_t sem1,sem2, sem3;

void myPrintf(char *str)
{
	while(*str != 0)
	{
		printf("%c", *str);
		fflush(stdout);
		str++;
		sleep(1);
	}
}
void* my_fun01(void *arg)
{	//P -1
	sem_wait(&sem1);
	myPrintf(arg);
	//V +1
	sem_post(&sem2);
}

void* my_fun02(void *arg)
{
	//P -1
	sem_wait(&sem2);
	myPrintf(arg);
	//V +1
	sem_post(&sem3);
}

void* my_fun03(void *arg)
{
	//P -1
	sem_wait(&sem3);
	myPrintf(arg);
	//V +1
	sem_post(&sem1);
}
int main()
{
	//初始化信号量 
	//第二参数0表示线程同步互斥,1表示进程同步互斥
	//第三个参数 是信号量的初始值
	sem_init(&sem1, 0, 1);//sem1先执行
	sem_init(&sem2, 0, 0);
	sem_init(&sem3, 0, 0);
	//创建两个线程
	pthread_t tid1,tid2,tid3;
	
	pthread_create(&tid1,NULL, my_fun01, "hello");
	pthread_create(&tid2,NULL, my_fun02, "world");
	pthread_create(&tid2,NULL, my_fun03, "来了 老弟");
	
	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
	
	//信号量撤销
	sem_destroy(&sem1);
	sem_destroy(&sem2);
	sem_destroy(&sem3);
	return 0;
}

运行结果:
在这里插入图片描述

7、无名信号量完成 有血缘关系的进程间 互斥和同步

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<semaphore.h>
#include<unistd.h>
#include <sys/mman.h>

void myPrintf(char *str)
{
	while(*str != 0)
	{
		printf("%c", *str);
		fflush(stdout);
		str++;
		sleep(1);
	}
}
int main()
{
	//互斥 只需要 定义一个信号量
	sem_t *sem;
	//匿名映射mmap MAP_ANONYMOUS  -1不用打开文件
	sem = mmap(NULL, sizeof(sem_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
	
	//信号量 sem 初始化 第一个1表示进程间同步互斥  第二个1信号量的初始值1
	sem_init(sem, 1, 1);
	
	pid_t pid = fork();
	if(pid == 0)//子进程
	{
		//p 操作-1
		sem_wait(sem);
		myPrintf("hello");
		//v 操作+1
		sem_post(sem);
	}
	else if(pid>0)//父进程
	{
		//p 操作-1
		sem_wait(sem);
		myPrintf("world");
		//v 操作+1
		sem_post(sem);
	}
	getchar();
	
	//销毁信号量
	sem_destroy(sem);
	//销毁mmap
	munmap(sem,sizeof(sem_t));
	return 0;
}

运行结果:
在这里插入图片描述

无名信号量完成 有血缘关系的进程间 同步:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<semaphore.h>
#include<unistd.h>
#include <sys/mman.h>

void myPrintf(char *str)
{
	while(*str != 0)
	{
		printf("%c", *str);
		fflush(stdout);
		str++;
		sleep(1);
	}
}
int main()
{
	//同步 只需要 定义两个信号量
	sem_t *sem1,*sem2;
	//匿名映射mmap MAP_ANONYMOUS  -1不用打开文件
	sem1 = mmap(NULL, sizeof(sem_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
	sem2 = mmap(NULL, sizeof(sem_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
	
	//信号量 sem 初始化 第一个1表示进程间同步互斥  第二个1信号量的初始值1
	sem_init(sem1, 1, 1);
	sem_init(sem2, 1, 0);
	
	pid_t pid = fork();
	if(pid == 0)//子进程
	{
		//p 操作-1
		sem_wait(sem1);
		myPrintf("hello");
		//v 操作+1
		sem_post(sem2);
	}
	else if(pid>0)//父进程
	{
		//p 操作-1
		sem_wait(sem2);
		myPrintf("world");
		//v 操作+1
		sem_post(sem1);
	}
	getchar();
	
	//销毁信号量
	sem_destroy(sem1);
	sem_destroy(sem2);
	
	//销毁mmap
	munmap(sem1,sizeof(sem_t));
	munmap(sem2,sizeof(sem_t));
	return 0;
}

运行结果:
在这里插入图片描述

总结:

无名信号量 (sem_t sem) 线程同步互斥。
无名信号量(mmap 匿名映射) 有血缘关系的进程同步互斥
有名信号量 无血缘关系的进程同步互斥

8、有名信号量 没有血缘进程互斥和同步

1、创建一个有名信号量

 #include <fcntl.h>           /* For O_* constants */
 #include <sys/stat.h>        /* For mode constants */
 #include <semaphore.h>
 //信号量存在
sem_t *sem_open(const char *name, int oflag);
//信号量不存在
sem_t *sem_open(const char *name, int oflag,
                       mode_t mode, unsigned int value);
功能:
创建一个信号量
参数:
name:信号量的名字
oflag:sem_open函数的权限标志
mode:文件权限(可读、可写、可执行 0777)的设置
value:信号量的初始值
返回值:
信号量的地址,失败:SEM_FAILED

2、信号量的关闭:

int sem_close(sem_t *sem);
功能:关闭信号量
参数:信号量的的地址
返回值:成功0  失败-1

3、信号量文件的删除

#include <semaphore.h>
int sem_unlink(const char *name);
功能:删除信号量的文件
参数:信号量的文件名
返回值:成功0 失败-1

4、P操作 sem_wait V操作sem_post 销毁信号量sem_destroy

有名信号量 没有血缘进程互斥:
fork01.c

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<semaphore.h>
#include<unistd.h>
#include<fcntl.h>

void myPrintf(char *str)
{
	while(*str != 0)
	{
		printf("%c", *str);
		fflush(stdout);
		str++;
		sleep(1);
	}
}
int main()
{
	//互斥需要一个信号量
	sem_t *sem = sem_open("sem", O_RDWR|O_CREAT,0666,1);
	//p操作-1
	sem_wait(sem);
	myPrintf("hello hehe haha");
	//v操作+1
	sem_post(sem);
	
	//关闭信号量
	sem_close(sem);
	return 0;
}

fork02.c

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<semaphore.h>
#include<unistd.h>
#include<fcntl.h>

void myPrintf(char *str)
{
	while(*str != 0)
	{
		printf("%c", *str);
		fflush(stdout);
		str++;
		sleep(1);
	}
}
int main()
{
	//互斥需要一个信号量
	sem_t *sem = sem_open("sem", O_RDWR|O_CREAT,0666,1);
	//p操作-1
	sem_wait(sem);
	myPrintf("world xixi lala");
	//v操作+1
	sem_post(sem);
	
	//关闭信号量
	sem_close(sem);
	return 0;
}

运行结果:编译的时候记得加-lpthread
在这里插入图片描述

有名信号量 没有血缘进程同步:
fork01.c

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<semaphore.h>
#include<unistd.h>
#include<fcntl.h>

void myPrintf(char *str)
{
	while(*str != 0)
	{
		printf("%c", *str);
		fflush(stdout);
		str++;
		sleep(1);
	}
}
int main()
{
	//同步需要2个信号量
	sem_t *sem1 = sem_open("sem1", O_RDWR|O_CREAT,0666,1);
	sem_t *sem2 = sem_open("sem2", O_RDWR|O_CREAT,0666,0);
	
	//p操作-1
	sem_wait(sem1);
	myPrintf("hello hehe haha");
	//v操作+1
	sem_post(sem2);
	
	//关闭信号量
	sem_close(sem1);
	sem_close(sem2);
	return 0;
}

fork02.c

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<semaphore.h>
#include<unistd.h>
#include<fcntl.h>

void myPrintf(char *str)
{
	while(*str != 0)
	{
		printf("%c", *str);
		fflush(stdout);
		str++;
		sleep(1);
	}
}
int main()
{
	//同步需要2个信号量
	sem_t *sem1 = sem_open("sem1", O_RDWR|O_CREAT,0666,1);
	sem_t *sem2 = sem_open("sem2", O_RDWR|O_CREAT,0666,0);
	//p操作-1
	sem_wait(sem2);
	myPrintf("world xixi lala");
	//v操作+1
	sem_post(sem1);
	
	//关闭信号量
	sem_close(sem1);
	sem_close(sem2);
	
	return 0;
}

运行结果:
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章