一、问题引入
首先我们先看如下代码:
int tickets = 10000;
void routine(const string &name)
{
while (true)
{
if (tickets > 0)
{
cout << name << "buy a ticket..,tickerts num:" <<tickets<< endl;
tickets--;
}
else
{
break;
}
}
}
int main()
{
//thread是我自己封装的一个类
//创建线程
thread t1("thread-1", routine);
thread t2("thread-2", routine);
thread t3("thread-3", routine);
thread t4("thread-4", routine);
//让线程跑起来
t1.start();
t2.start();
t3.start();
t4.start();
//等待线程
t1.join();
t2.join();
t3.join();
t4.join();
}
上述代码是一个多线程购票的代码,由于tickets是定义在主线程的栈空间中的,他可以被所有的线程看到,是一个共享资源,这个代码的初衷是让每个线程参与抢票,当票数为0的时候程序停止,但是我们发现运行结果的票数最后竟然出现了负数,这是怎么回事呢?
【解释】:
假设还剩最后一张票数的时候,线程1要进行if判断首先需要从内存中把tickets的值放到寄存器中,与0进行逻辑运算,线程1判断成功要继续执行代码,但是假设此时线程1时间片到了,轮到线程2执行了,由于线程1没有进行--操作,线程2从内存加载到寄存器的值还是为1,此时线程1和线程2都进入到了购票的代码中,但是票只有一张此时就出问题了,当线程1再次被调度时,会从寄存器中恢复数据,执行--操作,由于- -操作不是原子的,翻译成汇编会有3条指令,1.重读数据 2.--数据3. 写会数据,线程1从内存中重新读取票数为1,执行--操作后,写会内存,此时内存中的票数为0,当线程2在被调度时,从内存中的读取的票数就为0了,--操作后就会出现负数的情况,造成上述情况的根本原因就是因为多线程并发访问共享资源。
要解决以上问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临 界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么 只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux 上提供的这把锁叫互斥量。
二、互斥量
2.1进程线程间的互斥相关背景概念
• 临界资源:多线程执行流共享的资源就叫做临界资源
• 临界区:每个线程内部,访问临界资源的代码,就叫做临界区,保护临界资源的本质就是保护临界区
• 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源, 通常对临界资源起保护作用
• 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有 两态,要么完成,要么未完成
2.2 互斥量接口
初始化互斥量
初始化互斥量有两种方法:
方法 1,静态分配:
//当互斥量定义为全局的时候就可以直接用INITIALIZER初始化
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_mutex_lock函数就是给互斥量加锁,pthread_mutex_unlock是给互斥量解锁,两者中间的代码就是我们需要保护的临界区。当锁申请成功会继续向后执行,当锁申请失败,会阻塞等待,直到其他线程将解锁,重新参与竞争。
接下来我们重新改善一下上面的抢票函数:
//定义并初始化互斥量
pthread_mutex_t mutex=PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
void routine(const string &name)
{
while (true)
{
pthread_mutex_lock(&mutex);
if (tickets > 0)
{
cout << name << "buy a ticket..,tickerts num:" <<tickets<< endl;
tickets--;
pthread_mutex_unlock(&mutex);
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
}
}
三、互斥量实现原理探究
- 经过上面的例子,大家已经意识到单纯的 i++或者++i 都不是原子的,有可能 会有数据一致性问题
- 为了实现互斥锁操作,大多数体系结构都提供了 swap 或 exchange 指令,该指令 的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使 是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另 一个处理器的交换指令只能等待总线周期。 现在我们把 lock 和 unlock 的伪代码改 一下
- 所有的线程申请锁,前提是所有线程可以看到锁,所以锁也是个共享资源,这就要保证加锁解锁的过程是原子的,要么申请上锁了,要么没有,加锁的本质就是将内存中的互斥量置为0,让别的线程申请不到,由于互斥量只有1个,可以保证一个时刻只有一个线程申请到了互斥量。
问题:
加锁和解锁可以保证多线程串行访问临界资源,那在访问期间可不可以有线程切换呢?
【答案】:
这个期间是可以进行切换的,因为线程虽然被切换走了,但是并没有释放锁,其他线程想要访问临界资源依旧申请不到锁,只有线程释放锁以后其他线程才有机会访问临界区