进程线程间的互斥相关背景概念
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
同步:一般而言,让访问临界资源的过程在安全的前提下(一般都是互斥和原子的),让访问资源具有一定的顺序性(具有合理性)
互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。 多个线程并发的操作共享变量,会带来一些问题
上面是感性的认识,下面我们写一个抢票程序理性的认识一下
#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
#include<cstdio>
#include<cstdlib>
#define NUM 5 //5个线程
class Ticket
{
public:
Ticket()
:tickets(1000)
{
pthread_mutex_init(&mutex,nullptr); //初始化一把锁
}
bool GetTicket()
{
bool res = true;
// pthread_mutex_lock(&mutex);
if(tickets > 0)
{
usleep(1000);
cout<<"我是第"<<pthread_self()<<"个线程"<<":"<<"我抢到的票是"<<tickets<<endl;
tickets--;
printf("");
}
else
{
cout<<"票卖完了"<<endl;
//return false;
res = false;
}
// pthread_mutex_unlock(&mutex);
//sched_yield(); //如果不主动放弃cpu资源可能会出现一个线程一直在抢票
return res;
}
~Ticket()
{
pthread_mutex_destroy(&mutex);
}
private:
int tickets;
pthread_mutex_t mutex;
};
void* pthread_run(void* args)
{
Ticket * t = (Ticket* )args;
while(true)
{
if(!t->GetTicket())
break;
}
}
int main()
{
pthread_t tid[NUM];
Ticket * t = new Ticket();
for(size_t i = 0 ; i< NUM; ++i)
{
pthread_create(tid+i,nullptr,pthread_run,(void *)t); //创建5个线程
}
for(size_t i = 0; i < NUM;++i)
{
pthread_join(tid[i],nullptr); //销毁线程
}
return 0;
}
如果我们不加锁,那么可能会出现抢到负数的情况,这个情况线程是不安全的,例如一个航空公司只有100个座位,却买了120张票,那不是就完了吗?具体出现负数的情况如下图:
--
操作并不是原子操作,而是对应三条汇编指令:
load
:将共享变量
ticket
从内存加载到寄存器中
update
:
更新寄存器里面的值,执行
-1
操作
store
:将新值,从寄存器写回共享变量
ticket
的内存地址
例外我还可以发现多个线程抢到同一张票,全乱套了
要解决以上问题,需要做到三点:
代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临 界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。
Linux
上提供的这把锁叫互斥量。
![](https://i-blog.csdnimg.cn/blog_migrate/18858eec78e1c0b65bc9a0d70656069c.png)
互斥量的接口
初始化互斥量
初始化互斥量有两种方法:
方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法2,动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:mutex:要初始化的互斥量
attr:NULL
销毁互斥量
销毁互斥量需要注意:
使用
PTHREAD_ MUTEX_ INITIALIZER
初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用
pthread_ lock
时,可能会遇到以下情况
:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,
那么
pthread_ lock
调用会陷入阻塞
(
执行流被挂起
)
,等待互斥量解锁。
改进上面的售票系统
:
#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
#include<cstdio>
#include<cstdlib>
#define NUM 5 //5个线程
class Ticket
{
public:
Ticket()
:tickets(1000)
{
pthread_mutex_init(&mutex,nullptr); //初始化一把锁
}
bool GetTicket()
{
bool res = true;
pthread_mutex_lock(&mutex);
if(tickets > 0)
{
usleep(1000);
cout<<"我是第"<<pthread_self()<<"个线程"<<":"<<"我抢到的票是"<<tickets<<endl;
tickets--;
printf("");
}
else
{
cout<<"票卖完了"<<endl;
//return false;
res = false;
}
pthread_mutex_unlock(&mutex);
//sched_yield(); //如果不主动放弃cpu资源可能会出现一个线程一直在抢票
return res;
}
~Ticket()
{
pthread_mutex_destroy(&mutex);
}
private:
int tickets;
pthread_mutex_t mutex;
};
void* pthread_run(void* args)
{
Ticket * t = (Ticket* )args;
while(true)
{
if(!t->GetTicket())
break;
}
}
int main()
{
pthread_t tid[NUM];
Ticket * t = new Ticket();
for(size_t i = 0 ; i< NUM; ++i)
{
pthread_create(tid+i,nullptr,pthread_run,(void *)t); //创建5个线程
}
for(size_t i = 0; i < NUM;++i)
{
pthread_join(tid[i],nullptr); //销毁线程
}
return 0;
}
可以看出正常执行,但是有的小伙伴可能有疑问,为啥一个线程能一次抢多张票
因为当前线程在运行的时候其他线程是阻塞挂起的,当该线程解锁后仍然处于活跃状态,所以会抢到多票,不要完了操作系统进行线程切换也是需要代价的,但是问题又来了,锁也是一种资源,每个线程都会去抢锁,那么怎么能保证原子性呢?
互斥量实现原理探究
经过上面的例子,大家已经意识到单纯的
i++
或者
++i
都不是原子的,有可能会有数据一致性问题
为了实现互斥锁操作
,
大多数体系结构都提供了
swap
或
exchange
指令
,
该指令的作用是把寄存器和内存单 元的数据相交换,
由于只有一条指令
,
保证了原子性
,
即使是多处理器平台
,
访问内存的 总线周期也有先后
,
一 个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock
和
unlock
的伪 代码改一下
![](https://i-blog.csdnimg.cn/blog_migrate/f145f07a01057f12e5c7598322245d79.png)
具体过程如下图:
总结:先看到锁的线程,会把锁绑定到自己的上下文中,即使发生线程切换,线程也是抱着锁走的,前他人只有等待该线程执行完毕在抢锁
可重入VS线程安全
概念
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们 称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重 入函数,否则,是不可重入函数。
常见的线程不安全的情况
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
常见的线程安全的情况
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的 类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
调用了
malloc/free
函数,因为
malloc
函数是用全局链表来管理堆的
调用了标准
I/O
库函数,标准
I/O
库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
常见可重入的情况
不使用全局变量或静态变量
不使用用
malloc
或者
new
开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生 死锁,因此是不可重入的。