说明:
- 本文是翻译自《MultiThreaded-Programming-With-POSIX》,作者Guy Kerens。
- 本文预计翻译三章,主要涉及pthread基本知识、互斥量(锁)和条件变量,一是因为这已经能够引导读者入门,二是因为本人在工作之余翻译,实在时间捉急。
- 翻译:张小川,转载请保留原作者
使用互斥量同步线程
当在同一个内存空间跑几个线程时的一个基本问题就是保证它们不会同时操作同一内存(step on each other's toes
).这一点,我们指的是使用两个不同线程的数据结构问题。
例如,考虑这样一个例子:两个线程尝试更新两个变量。一个线程将两个值都设为0,另一个线程将两个值都设为1。如果两个线程同时想要访问这两个数据,我们可能会得到一个0一个1.这是因为在一个线程将第一个线程置零后可能会发生一个上下文切换(contex-switch),切换之后第二个线程将两个变量都置为1,而当再次切换到第一个线程后,它仅将第二个变量置零,这样我们就得到了一个0一个1.
什么是互斥量
互斥量(或称为互斥锁,以下在不影响阅读的情况下不加以区分)是pthread 库为解决这个问题提供的一个基本的机制。互斥量是一个锁,它保证如下三件事情:
- 原子性-锁住一个互斥量是一个院子操作,表明操作系统(或者线程库)保证如果你锁了一个互斥量,那么在同一时刻就不会有其他线程能够锁住这个互斥量;
- 奇异性-如果一个线程锁住了一个互斥量,那么可以保证的是在该线程释放这个锁之前没有其他线程可以锁住这个互斥量;
- 非忙等待-如果一个线程(线程1)尝试去锁住一个由线程2锁住的锁,线程1会挂起(suspend)并且不会消耗任何CPU资源,直到线程2释放了这个锁。这时,线程1会唤醒并继续执行,锁住这个互斥量。
从这三点可以看到一个互斥锁是如何保证对变量(或一般的关键代码段)的互斥访问的。下面给出了上节讨论的如何更新两个变 量的伪代码。
第一个线程使用:
lock mutex 'X1'
set 1st variable to '0'
set 2nd variable to '0'
unlock mutex 'X1'
第二个线程使用:
lock mutex 'X1'
set 1st variable to '1'
set 2nd variable to '1'
unlock mutex 'X1'
假定两个线程使用相同的互斥量,那么可以保证的是在两个线程都跑过这段代码之后,两个变量要么全是0要么全1.你应该注意到这需要程序员来做一些工作——如果第三个线程通过不适用这个互斥量的其他代码来修改这些变量,仍然可能弄乱这两个变量的值。因此,将访问这两个变量的所有代码都放入一个小的函数集合中,并且只通过这些函数来访问这两个变量就变得至关重要。
创建和初始化互斥量
在使用互斥量之前需要声明一个pthread_mutex_t
类型的变量,然后对其初始化。最简单的方法就是用常量PTHREAD_MUTEX_INITIALIZER
对其赋值。所以我们的代码会是如下形式:
pthread_mutex_t a_mutex = PTHREAD_MUTEX_INITIALIZER;
这儿需要注意一点的是,这种初始化方法创建的互斥量称为快互斥量(‘fast mutex’)。这表明如果一个线程锁住了这个互斥锁,然后尝试再次对其上锁,它会卡住——它就处于一个死锁状态。
另一种类型的锁称为‘递归锁’(’recursive mutex’),它允许锁住这个互斥量的线程锁住它多次而不被卡住(但是其他尝试锁住这个锁的线程会被卡住)。如果该线程解锁了这个互斥量,它仍然是锁住的状态,直到它解锁的次数与锁住的次数是相同的。这与现代的锁是一样的——如果你顺时针方向转两次锁住了锁,那么你开锁的时候就需要逆时针方向转两次。这种类型的互斥量用常量PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP
对其赋值。
对一个互斥量上锁和解锁
使用函数pthread_mutex_lock()
函数来锁住一个互斥量。这个函数尝试锁住一个互斥量,如果其他线程已经锁住了这个互斥量那么它会挂起。这种情况下,当那个线程解锁了这个互斥量后,你的线程才会锁住这个锁。如下给出了锁住一个互斥量的示例(假定他已经初始化了):
int rc = pthread_mutex_lock(&a_mutex);
if (rc)
{
/* an error has occurred */
perror("pthread_mutex_lock");
pthread_exit(NULL);
}
/* mutex is now locked - do your stuff. */
.
.
一个线程做完它的事情(改变变量或者数据结构的值,处理文件或者其他它要做的事情)之后,它应该解锁该互斥量,使用函数pthread_mutex_unlock()
:
rc = pthread_mutex_unlock(&a_mutex);
if (rc)
{
perror("pthread_mutex_unlock");
pthread_exit(NULL);
}
销毁一个互斥量
在使用完一个互斥量之后,需要销毁它。用完表示没有线程再使用它了。如果仅有一个线程用完,那么仍然要保持它的有效,因为其他线程可能会使用它。一旦所有线程都使用完毕,那么最后一个线程可以使用pthread_mutex_destroy()
函数销毁它:
rc = pthread_mutex_destroy(&a_mutex);
在调用这个函数之后,这个互斥量就不能再用了,除非再次进行初始化。因此,如果一个线程过早地销毁一个互斥量,其它线程尝试访问(锁住或解锁)它的时候,这个线程的上锁或解锁函数就会返回一个EINVAL
错误。
使用互斥量——一个完整例子
在了解了一个互斥量的所有生命周期之后,那么来看一个使用互斥量的例子。这个程序描述了两个员工竞选“今日之星”的头衔,和其所代表的荣誉。为了快速模拟,我们使用了三个线程:一个提拔Danny为“今日之星”,一个提拔Moshe为“今日之星”,第三个线程保证“今日之星”员工的内容是一致的(例如:包含一个员工的数据)。
我们一共提供了两份代码,一份使用了互斥量,一份没有。尝试一下这两份代码来看一下不同之处,这样就会确信互斥量在多线程环境中是至关重要的。
这两份代码分别是employee-withmutex.c
和employee-without-mutex.c
(代码请参考原文)