临界资源:多线程执行流共享的资源就叫做临界资源。
临界区:每个线程内部,访问临界自娱的代码,就叫做临界区。
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。
为了避免带来的问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
1、我们需要先定义一个全局的互斥量:
pthread_mutea_t mutex;
2、在主函数中,我们需要将互斥量初始化:
pthread_mutex_init(&mutex, NULL);
3、在对临界区进行操作时,需要对互斥量加锁和解锁:
pthread_mutex_lock(&mutex);//加锁
pthread_mutex_unlock(&mutex);//解锁
返回值:成功返回0,失败返回错误号。
4、最后需要在主函数中销毁互斥量:
pthread_mutex_destroy(&mutex);
我们可以看到,互斥量(锁资源)必须被所有成员看到,所以锁也是临界资源,所以说加锁和解锁过程也应该是原子的,这个过程的原子性是怎么实现的呢?看下面两段伪代码。
pthread_mutex_lock函数实现的伪代码:
lock:
movb $0, %al //把al清零
xchgb %al, mutex //交换al和mutex的值
if (al寄存器的内容 > 0)
return 0;
else
挂起等待
goto lock;
当线程1执行xchgb %al, mutex
代码时,把mutex的值(1)和al
的值(0)交换,假如这个时候线程1的时间片到了,线程2来了,则把此时al
的值放到线程1的栈中便于线程1下次执行。线程2执行xchgb %al, mutex
代码时,因为mutex的值已经是0,所以交换后al
的值依然是0,结果if语句判断后挂起等待。所以只有线程1加锁成功,最后返回0。当线程1解锁之后,线程二结束挂起,执行goto lock;
语句,跳到lock
处重新申请锁。
如下图所示:
pthread_mutex_unlock函数实现的伪代码:
unlock:
movb $1, mutex
唤醒等待mutex的线程;
return 0;
当线程1执行完movb $1, mutex
语句时,假如说线程2开始执行了,这个时候无论线程2是之前在等待的还是未等待的线程,它申请锁操作都不会影响线程1了,因为这个时候线程1关于mutex的操作已经结束了。(主要是我们在代码中对mutex的操作代码只有一句)。
上面对两段伪代码的分析解释了为什么加锁和解锁操作是原子的。