依然以抢票问题为例。前面的文章从提出问题,到发现问题,而本文则是解决问题。通常解决问题的方式不止一种,但是为了避免复杂化,本文只讲互斥量。
1. 互斥量
1.1 基本概念
为了确保同一时间只有一个线程访问数据,在访问共享资源前需要对互斥量上锁。一旦对互斥量上锁后,任何其他试图再次对互斥量上锁的线程都会被阻塞,即进入等待队列。
上面的文字用伪代码表示:
有些同学可能觉得通过代码很容易实现,其实不然。比方说下面这样:
上面这种做法是错误的,你有没有想过,标记 flag 也是共享资源?
实际上,有一种称之为 peterson 的算法可以解决两线程互斥问题,它的原理并不容易,如果你对此有兴趣,请参考《深入理解互斥锁的实现》。
1.2 互斥量的数据类型
pthread 中,互斥量是用 pthread_mutex_t 数据类型表示的,通常它是一个结构体。在使用它前,必须先对它进行初始化。有两种方法可以对它进行初始化:
- 通过静态分配的方法,将它设置为常量
PTHREAD_MUTEX_INITIALIZER
. - 使用函数
pthread_mutex_init
进行初始化,如果是用此种方法初始化的互斥量,用完后还需要使用pthread_mutex_destroy
对其进行回收。
在上面的函数中,有两点需要提一下:
- (1) restrict 关键字的含义:访问指针 mutex 指针的内容的唯一方法是使用 mutex 指针。通常这是告诉编译器:除了 mutex 指针指向这个内存,再也没别的指针指向这里了。
- (2) pthread_mutexattr_t 类型,用来描述互斥量的属性。现阶段,attr 指针默认设置为 NULL.
1.3 互斥量的加锁和解锁
在第 1.1 节中说,如果试图对一个已加锁的互斥量上锁,会让线程阻塞进入等待队列,实际上这是对 pthread_mutex_lock 函数说的。
如果使用 pthread_mutex_trylock,无论互斥量之前有没有上锁,线程会立即返回而不会阻塞,它是通过返回值来判断是否上锁成功:
- 如果 pthread_mutex_trylock 返回0, 表示上锁成功。
- 如果 pthread_mutex_trylock 返回EBUSY,表示上锁失败。
所以这两种上锁的函数唯一区别就是一个是阻塞函数,另一个是非阻塞函数。不过通常不使用非阻塞版本的,它会浪费 cpu,除非你别有用意。
有同学可能会好奇,为什么我们自己用一个共享的 flag 变量做标记不行,而这里的 lock 函数却可以做到?这是因为 lock 函数对 mutex 的操作是原子的,所谓的原子操作,就是要么一次执行成功,要么一次执行失败。而你使用全局 flag 变量,是做不到这一点的,从 if (flag == 0) 到 flag = 1的赋值操作是分成了两个步骤,翻译成汇编语句那就需要更多条了。所以如果你想自己实现这样的原子操作,就只能使用汇编语句来编写啦,有可能的话,后面我自己用代码来实现一个!(当然用关中断也可以,只不过没必要如此麻烦。)
2. 解决抢票问题
说了一大堆的概念,小伙伴可能已经迫不急待的想看代码了,这的确是一种速度最快的方式^_^
2.1 程序清单
2.2 编译和运行
图1 运行结果
从图 1 中可以看到,3 张票是被正常的抢走了,没有产生多抢的现象。luffy 还是比较厉害一点,抢了 2 张票,而 allen 只抢到了一张票,悲催……
3. 总结
- 理解什么是互斥量,它的作用是什么
- 掌握两种初始化互斥量的方法
- 掌握两种对互斥量加锁的方法
- 掌握解锁方法
练习 1:使用 pthread_mutex_init 函数初始化互斥量,记得用完后回收。
练习 2:使用非阻塞版本的加锁函数修改本文中的实验。