提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
Linux线程互斥
提示:这里可以添加本文要记录的大概内容:
前面我们在 Linux线程基本概念 介绍了线程基本概念,在Linux线程控制中介绍了线程创建,线程终止,线程等待,线程分离等等概念,今天我们来介绍一下线程互斥的相关概念,线程并发带来的问题
提示:以下是本篇文章正文内容,下面案例可供参考
一、互斥概念
我们之前在 进程间通信之匿名管道 讲管道相关概念时我们提到了一些与进程线程间互斥相关的背景概念,我们今天站在线程的角度来回忆一下
临界资源:多线程执行流共享的资源叫做临界资源
临界区:每个线程内部访问临界资源的代码叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两种状态,要么完成,要么未完成
二、并发问题
并发在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
并发可能存在某一时刻会对共享资源进行同时访问,如果在对这些共享资源不遵循规则,那么就可能对这些共享资源造成破坏。接下来我们使用一个最简单的代码来阐述并发出现的问题。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <stdlib.h>
#include <pthread.h>
char buff[100];
int i;
//创建奇效验和偶校验进程
void *Odd_number(void *o)
{
for (i = 0; i < 100; i++)
{
if (buff[i] % 2 == 1)
{
sleep(1);
printf("奇数=%d\n", buff[i]);
}
}
}
void *Even_number(void *e)
{
for (i = 0; i < 100; i++)
{
if (buff[i] % 2 == 0)
{
sleep(1);
printf("偶数=%d\n", buff[i]);
}
}
}
int main(void)
{
int rec1, rec2, n;
memset(buff, 0, 100);
for (i = 0; i < 100; i++)
{
buff[i] = i;
}
pthread_t Odd, Even;
rec1 = pthread_create(&Odd, NULL, Odd_number, (void *)buff);
if (rec1 != 0)
{
printf("创建奇数线程失败");
return -1;
}
rec2 = pthread_create(&Even, NULL, Even_number, (void *)buff);
if (rec2 != 0)
{
printf("创建偶数线程失败");
return -1;
}
printf("Odd=%lu,Even=%lu\n", Odd, Even);
while (1)
{
/* code */
}
printf("等待子线程退出。\n");
pthread_join(Odd, NULL);
pthread_join(Even, NULL);
printf("子线程已退出。\n");
return 1;
}
执行结果:
Odd=139867205027584,Even=139867196634880
偶数=0
奇数=2
偶数=3
奇数=4
奇数=5
偶数=7
偶数=8
奇数=10
奇数=11
偶数=13
偶数=14
奇数=16
奇数=17
偶数=19
偶数=20
上述问题我们也清晰的看到了,那为什么会引发这样的问题呢,其实最主要的原因还是对全局变量total++这一个操作并不是原子性的,它分为三个个汇编指令,load:将共享变量从内存加载到寄存器,update:对变量进行+1操作,store:再将共享变量由回寄存器写回内存
二、互斥量mutex
我们要解决上述问题,需要做到以下三点:
1、代码必须要有互斥行为,当代码进入临界区时,不允许其他线程进入该临界区
2、如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
3、如果线程不在临界区中执行, 那么该线程不能阻止其他线程进入临界区
要做到这三点,本质上就是需要一把锁,linux上提供了这把锁叫互斥量metux
互斥量接口
初始化互斥量
静态分配
动态分配
销毁互斥量
销毁互斥量需要注意:
使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保之后不会有任何线程再尝试加锁
互斥量的加锁和解锁
使用pthread_mutex_lock时可能会出现以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁
我们将刚刚的代码修改一下,给它加上互斥锁
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <stdlib.h>
#include <pthread.h>
char buff[100];
int i;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//创建奇效验和偶校验进程
void *Odd_number(void *o)
{
for (i = 0; i < 100; i++)
{
pthread_mutex_lock(&mutex);
if (buff[i] % 2 == 1)
{
printf("奇数=%d\n", buff[i]);
}
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
void *Even_number(void *e)
{
for (i = 0; i < 100; i++)
{
pthread_mutex_lock(&mutex);
if (buff[i] % 2 == 0)
{
printf("偶数=%d\n", buff[i]);
}
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
int main(void)
{
int rec1, rec2, n;
memset(buff, 0, 100);
for (i = 0; i < 100; i++)
{
buff[i] = i;
}
pthread_mutex_init(&mutex, NULL);
pthread_t Odd, Even;
rec1 = pthread_create(&Odd, NULL, Odd_number, (void *)buff);
if (rec1 != 0)
{
printf("创建奇数线程失败");
return -1;
}
rec2 = pthread_create(&Even, NULL, Even_number, (void *)buff);
if (rec2 != 0)
{
printf("创建偶数线程失败");
return -1;
}
printf("Odd=%lu,Even=%lu\n", Odd, Even);
while (1)
{
/* code */
}
printf("等待子线程退出。\n");
pthread_join(Odd, NULL);
pthread_join(Even, NULL);
pthread_mutex_destroy(&mutex);
printf("子线程已退出。\n");
return 0;
}
互斥锁的原理
互斥锁的实现原理实际上非常的简单,他并没有你想的那么复杂,这里我们必须要知道的前提是我们可以将汇编的一句指令认为是原子的,所以为了实现锁的互斥操作,大多数体系结构对于互斥锁的实现使用了swap或者exchange指令,该指令的作用是把寄存器和单元数据交换,该指令只有一句,所以是原子操作,因为就算是多线程,总线周期也有先后,所以利用此指令总能保证同一个时间只有一个线程进入临界区。
现在我们来看看如何使用swap或者exchange指令实现互斥锁原理,来看下面的一段伪代码:可以看到锁资源最开始拿到的值为1,然后使用swap或exchange指令将寄存器中的值和?拥有的值交换,如果寄存器中现在值为1,那么表示资源申请成功,如果寄存器中值为0,说明?资源中的1已经被其他线程交换走了,所以当前进程也就需要进入等待队列
二、重入
我们上面加加变量的小栗子有问题其实另一个原因是他本质是一个不可重入的函数,什么是重入?重入是指同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下不会出现不同或者任何问题,则我们称其为可重入函数,否则反之称为不可重入函数。
常见不可重入的情况:
调用了malloc和free函数,因为malloc是使用全局链表来管理堆的
调用了标准I/O库函数,I/O库很多函数的实现都使用了全局的数据结构
函数体使用了静态全局变量
常见可重入的情况:
不使用全局变量和静态变量
不适用malloc或者new开辟出的空间
不调用不可重入函数
二、线程安全问题
线程安全是指多个线程并发执行一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,如果没有互斥锁的保护的情况下,会出现该问题。
常见线程不安全的情况:
不保护共享变量的函数
函数的状态随着被调用,函数发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
常见线程安全的情况:
每个线程对于全局变量或者静态变量只有读取的权限,而没有写入的权限
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
线程安全 VS 重入
很经典的一句话,可重入函数一定是线程安全的,但是线程安全的不一定是可重入函数
函数是不可重入的,那么就不能由多个线程使用,有可能引发线程安全的问题
如果对临界资源访问加上锁,则这个函数线程安全,但是如果这个重入函数若锁没有释放则会产生死锁问题