为什么需要互斥量?
大部分情况下,线程使用的数据都是局部变量,变量的地址在线程栈空间内,这种情况下,变量属于单个线程,其他线程无法获取这种变量。
如果所有的变量都是如此,将会省去无数的麻烦。但实际的情况是,很多变量都是多个线程共享的,这样的变量称为共享变量。可以通过数据得到共享,完成多个线程之间的交互与通信。
但是多个线程并发的操作共享变量会带来问题。
++操作,并不是一个原子操作(atomic operation),而是对应了如下三条汇编指令。
·Load:将共享变量global_cnt从内存加载进寄存器,简称L。
·Update:更新寄存器里面的global_cnt值,执行加1操作,简称U。
·Store:将新的值,从寄存器写回到共享变量global_cnt的内存地址,简称为S。
多线程在不加锁的情况下访问共享资源会产生难以预料的结果
应该避免多个线程同时操作共享变量,对于共享变量的访问,包括读写,都必须被限制为每次只有一个线程来执行。
锁是一个普遍的需求,但是让用户从零开始实现一个正确的锁也并不容易。正是因为这种需求具有普遍性,所以Linux提供了互斥量。
互斥量的接口
1.互斥量的初始化
互斥量采用的是英文mutual exclusive的缩写,即mutex。
正确的使用互斥量来保护共享数据,首先要定义和初始化互斥量。POSIX提供了两种初始化互斥量的方法。
第一种方法是将PTHREAD_MUTEX_INITIALIZER赋值给定义的互斥量如下:
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
如果互斥量是动态分配的,或者需要设定互斥量的属性,那么上面静态初始化的方法就不适用了,NPTL提供了另外的函数pthread_mutex_init()对互斥量进行动态的初始化:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
第二个pthread_mutexattr_t指针的入参,是用来设定互斥量的属性的,传递NULL即可,表示使用互斥量的默认属性。
调用pthrerad_mutex_init()之后,互斥量处于没有加锁的状态。
2.互斥量的销毁
在确定不再需要互斥量的时候,就要销毁它。但是在销毁之前,有三点要注意:
- 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量无须销毁
- 不要销毁一个已加锁的互斥量,或者是真正配合条件变量使用的互斥量
- 已经销毁的互斥量,要确保后面不会有线程在尝试加锁
销毁互斥量的接口如下:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
当互斥量处于已加锁的状态,或者正在和条件变量配合使用,调用pthread_mutex_destroy函数会返回EBUSY错误码。
3.互斥量的加锁和解锁
POSIX提供了如下的接口:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
临界区的大小
现在我们已经意思到需要用锁来保护共享变量。不过还另有一个需要注意的事项,即合理地设定临界区的范围。
第一临界区的范围不能太小,如果太小,可能起不到保护的目的。考虑如下的场景,如果哈希表中不存在某元素,那么向哈希表中插入某元素,代码如下:
if(!htable_contain(hashtable,elem.key))
{
pthread_mutex_lock(&mutex);
htable_insert(hashtable,&elem);
pthread_mutex_lock(&mutex);
}
从表面上看,哈希表是互斥的,插入时有锁的保护,但是对于是否有相同的元素的判断却没有加锁(即临界区较小),不能够达到预期的目的。当然临界区也不能太大,临界区的代码不能并行,如果临界区太大就会无法发挥多处理器多线程的优势。