进程线程互斥相关概念
临界资源:多线程执行流共享的资源叫做临界资源
临界区:每个线程内部,访问临界资源的代码,叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
原子性:不会被任何调度机制打断的操作,该操作只有两种状态:要么完成,要么未完成
例如实现售票系统:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include <stdlib.h>
int tickets=200;
void *GetTicket(void *arg)
{
while(1)
{
if(tickets>0)//说明有票
{
usleep(100000);//以微秒来睡眠
printf(" get a ticket no is : %d\n",tickets--);
}
else
{
printf("%s............quit\n",(char*)arg);
break;
}
}
pthread_exit((void*)0);
}
int main()
{
pthread_t tid1,tid2,tid3,tid4;
pthread_create(&tid1,NULL,GetTicket,(void*)"thread 1");
pthread_create(&tid2,NULL,GetTicket,(void*)"thread 2");
pthread_create(&tid3,NULL,GetTicket,(void*)"thread 3");
pthread_create(&tid4,NULL,GetTicket,(void*)"thread 4");
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
pthread_join(tid4,NULL);
}
它的执行结果是:
get a ticket no is : 200
get a ticket no is : 199
get a ticket no is : 198
get a ticket no is : 197
get a ticket no is : 196
get a ticket no is : 195
get a ticket no is : 194
get a ticket no is : 193
...
get a ticket no is : 4
get a ticket no is : 3
get a ticket no is : 2
get a ticket no is : 1
thread 3............quit
get a ticket no is : 0
thread 4............quit
get a ticket no is : -1
thread 2............quit
get a ticket no is : -2
thread 1............quit
段错误(吐核)
一共有200张票,但是卖出去的票比200多,可能有多个线程同时访问tickets这个临界资源,if语句判断条件为真后,代码可以并发的切换到其他线程,usleep的漫长过程中,可能有很多个线程进入该代码,并且ticket–这个操作本身也不是一个原子操作,而是对应3条汇编指令:
(1)将共享变量tickets从内存加载到CPU内部的寄存器中;(CPU内部有若干类寄存器,通用的有状态寄存器,指令寄存器、程序计数器)
(2)更新CPU内部的寄存器中的值,执行-1操作;
(3)将新值从CPU寄存器写回共享变量的内存地址中。
因此,要解决上述问题,要做到
(1)代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区
(2)如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
(3)如果线程不在临界区执行,那么该线程不能阻止其他线程进入临界区
上述这三点就是一把锁,Linux提供的这把锁叫做互斥量
互斥量
在大部分情况下,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这样变量归属于单个线程,其他线程无法获得这种变量;但是有时候,很多变量要在线程间共享,这样的变量称为共享变量(上述售票系统中的tickets就是共享变量),可以通过数据的共享完成线程之间的交互,这同时也会带来上述的问题。
互斥量就是一把锁,如图:
给临界区加上锁,保证了线程对临界区的原子性。
- 互斥量的接口
1、初始化互斥量
初始化互斥量有两种方法:
(1)静态分配
例如:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
使用PTHREAD_MUTEX_INITALIZER这个宏来进行初始化
(2)动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
其中参数mutex表示要初始化的互斥量,attr表示属性,一般是NULL;
2、销毁互斥量
函数原型是:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:
(1)使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁;
(2)不要销毁一个已经加锁的互斥量
(3)已经销毁的互斥量,确保后面不会再有线程尝试加锁
3、互斥量加锁和解锁
函数原型是:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数mutex表示互斥量。
在调用pthread_mutex_lock时,会遇到以下情况:
(1)互斥量未锁,该函数将互斥量锁定,同时返回成功;
(2)发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_mutex_lock调用陷入阻塞(执行流挂起),等待互斥量解锁。
这样我们就可以给刚刚的售票系统加锁,例如:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include <stdlib.h>
int tickets=200;
pthread_mutex_t lock;//创建锁
void *GetTicket(void *arg)
{
while(1)
{
pthread_mutex_lock(&lock);
if(tickets>0)//说明有票
{
usleep(100000);//以微秒来睡眠
printf(" get a ticket no is : %d\n",tickets--);
pthread_mutex_unlock(&lock);//解锁
}
else
{
printf("%s............quit\n",(char*)arg);
pthread_mutex_unlock(&lock);
break;
}
}
pthread_exit((void*)0);
}
int main()
{
pthread_mutex_init(&lock,NULL);//初始化互斥量
pthread_t tid1,tid2,tid3,tid4;
pthread_create(&tid1,NULL,GetTicket,(void*)"thread 1");
pthread_create(&tid1,NULL,GetTicket,(void*)"thread 2");
pthread_create(&tid1,NULL,GetTicket,(void*)"thread 3");
pthread_create(&tid1,NULL,GetTicket,(void*)"thread 4");
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
pthread_join(tid4,NULL);
pthread_mutex_destroy(&lock);//销毁互斥量
}
最终就不会有多余的票,而且运行速度变慢,因为实现了加锁和解锁,加锁会占用系统资源程序运行时性能变低,lock的加锁和解锁一定要做到原子性,因为它也是临界资源(每个线程在访问临界区之前都要先访问锁,因此是临界资源)。
- 互斥量实现原理
单纯的i++或者++i不是原子的,可能会出现数据一致性问题,为了实现互斥锁的操作,大多数体系结构都提供了swap或者exchange指令,该指令作用是将CPU寄存器和内存单元的数据相交换,因为只有一条指令,保证了原子性;
假设Mutex变量的值为1表示互斥锁空闲,这时某个进程调用lock可以获得锁,而Mutex的值为0表示互斥锁已经被某个线程获得,其他线程再调用lock只能挂起等待,我们来看一段lock和unlock的伪代码:
lock:
if(mutex>0)
{
mutex=0;
return 0;
}
else
//挂起等待
goto lock;
unlock:
mutex=1;
//唤醒等待Mutex的线程
return 0;
因为unlock过程一定是原子的,所以注意lock过程,unlock操作中唤醒等待线程的步骤可以有不同的实现,可以只唤醒一个等待线程,也可以唤醒所有等待该Mutex的线程,然后让被唤醒的这些线程去竞争获得Mutex,竞争失败的线程继续等待。
但是上述伪代码中lock函数中对Mutex的读取、判断和修改是非原子操作,如果两个线程同时调用lock,这时Mutex是1,而两个线程都判断mutex>0成立,然后其中一个线程置mutex=0,另一个线程并不知道这一情况,也置mutex=0,于是两个线程都以为自己获得了锁,因为线程在任何时候都可能触发导致切换,而在每个线程运行时,mutex都会首先被读到CPU里的寄存器中,这将导致mutex的副本过多,数据无法保持一致性,因此这种伪代码的实现是错误的。
因此来看另一种实现方法
首先利用汇编指令令xchgb将已经置为0的寄存器a1与互斥量mutex值进行交换,只对寄存器操作,不会产生物理内存中mutex的副本,这样即使线程在执行xchgb指令和条件判断时被切换出去,也是没有任何意义的,对程序结果没有影响,unlock中释放锁操作同样只用一条指令实现,保证了它的原子性,这就是lock和unlock的实现原理。
注意:每个Mutex都有一个等待队列,一个线程要在Mutex上挂起等待,首先要把自己(PCB)加入等待队列中,然后置线程状态为睡眠状态,然后调用调度器函数切换到别的线程,一个线程要唤醒等待队列中的其它线程,只需要从等待队列中取出一项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有可能切换到被唤醒的线程。
源代码(github):
https://github.com/wangbiy/Linux2/commit/eb12b0ba74896d0593f374c3a3ea2287f894bf8e