示例 4–1 显示了使用互斥锁定的一些代码段。
#include <pthread.h>
pthread_mutex_t count_mutex;
long long count;
void
increment_count()
{
pthread_mutex_lock(&count_mutex);
count = count + 1;
pthread_mutex_unlock(&count_mutex);
}
long long
get_count()
{
long long c;
pthread_mutex_lock(&count_mutex);
c = count;
pthread_mutex_unlock(&count_mutex);
return (c);
}
示例 4–1 中的两个函数将互斥锁用于不同目的。increment_count() 函数使用互斥锁确保对共享变量进行原子更新。get_count() 函数使用互斥锁保证以原子方式读取 64 位数量 count。在 32 位体系结构上,long long 实际上是两个 32 位数量。
读取整数值时执行的是原子运算,因为整数是大多数计算机中常见的字长。
有时,可能需要同时访问两个资源。您可能正在使用其中的一个资源,随后发现还需要另一个资源。如果两个线程尝试声明这两个资源,但是以不同的顺序锁定与这些资源相关联的互斥锁,则会出现问题。例如,如果两个线程分别锁定互斥锁 1 和互斥锁 2,则每个线程尝试锁定另一个互斥锁时,将会出现死锁。示例 4–2 说明了可能的死锁情况。
|
线程 1 |
线程 2 |
|
|---|---|---|
|
pthread_mutex_lock(&m1);
/* use resource 1 */
pthread_mutex_lock(&m2);
/* use resources 1 and 2 */ pthread_mutex_unlock(&m2); pthread_mutex_unlock(&m1); |
pthread_mutex_lock(&m2);
/* use resource 2 */
pthread_mutex_lock(&m1);
/* use resources 1 and 2 */ pthread_mutex_unlock(&m1); pthread_mutex_unlock(&m2); |
|
避免此问题的最佳方法是,确保线程在锁定多个互斥锁时,以同样的顺序进行锁定。如果始终按照规定的顺序锁定,就不会出现死锁。此方法称为锁分层结构,它通过为互斥锁指定逻辑编号来对这些锁进行排序。
另外,请注意以下限制:如果您持有的任何互斥锁其指定编号大于 n,则不能提取指定编号为 n 的互斥锁。
但是,不能始终使用此方法。有时,必须按照与规定不同的顺序提取互斥锁。要防止在这种情况下出现死锁,请使用 pthread_mutex_trylock()。如果线程发现无法避免死锁时,该线程必须释放其互斥锁。
|
线程 1 |
线程 2 |
|---|---|
|
pthread_mutex_lock(&m1); pthread_mutex_lock(&m2);
/* no processing */
pthread_mutex_unlock(&m2); pthread_mutex_unlock(&m1); |
for (; ;) { pthread_mutex_lock(&m2);
if(pthread_mutex_trylock(&m1)==0) /* got it */ break; /* didn't get it */ pthread_mutex_unlock(&m2); } /* get locks; no processing */ pthread_mutex_unlock(&m1); pthread_mutex_unlock(&m2); |
在示例 4–3 中,线程 1 按照规定的顺序锁定互斥锁,但是线程 2 不按顺序提取互斥锁。要确保不会出现死锁,线程 2 必须非常小心地提取互斥锁 1。如果线程 2 在等待该互斥锁释放时被阻塞,则线程 2 可能刚才已经与线程 1 进入了死锁状态。
要确保线程 2 不会进入死锁状态,线程 2 需要调用 pthread_mutex_trylock(),此函数可在该互斥锁可用时提取它。如果该互斥锁不可用,线程 2 将立即返回并报告提取失败。此时,线程 2 必须释放互斥锁 2。线程 1 现在会锁定互斥锁 2,然后释放互斥锁 1 和互斥锁 2。
示例 4–4 和示例 4–5 说明了如何同时提取三个锁。通过按照规定的顺序提取锁可避免出现死锁。
typedef struct node1 {
int value;
struct node1 *link;
pthread_mutex_t lock;
} node1_t;
node1_t ListHead;
本示例针对每个包含一个互斥锁的节点使用单链接列表结构。要将某个节点从列表中删除,请首先从 ListHead 开始搜索列表,直到找到所需的节点为止。ListHead 永远不会被删除。
要防止执行此搜索时产生并发删除,请在访问每个节点的任何内容之前先锁定该节点。由于所有的搜索都从 ListHead 开始,并且始终按照列表中的顺序提取锁,因此不会出现死锁。
因为更改涉及到两个节点,所以找到所需的节点之后,请锁定该节点及其前序节点。因为前序节点的锁总是最先提取,所以可再次防止出现死锁。示例 4–5 说明如何使用 C 代码来删除单链接列表中的项。
node1_t *delete(int value)
{
node1_t *prev, *current;
prev = &ListHead;
pthread_mutex_lock(&prev->lock);
while ((current = prev->link) != NULL) {
pthread_mutex_lock(¤t->lock);
if (current->value == value) {
prev->link = current->link;
pthread_mutex_unlock(¤t->lock);
pthread_mutex_unlock(&prev->lock);
current->link = NULL;
return(current);
}
pthread_mutex_unlock(&prev->lock);
prev = current;
}
pthread_mutex_unlock(&prev->lock);
return(NULL);
}
示例 4–6 通过将以前的列表结构转换为循环列表来对其进行修改。由于不再存在用于标识的头节点,因该线程可以与特定的节点相关联,并可针对该节点及其邻居执行操作。锁分层结构在此处不适用,因为链接之后的分层结构明显是循环结构。
typedef struct node2 {
int value;
struct node2 *link;
pthread_mutex_t lock;
} node2_t;
以下的 C 代码用来获取两个节点上的锁并执行涉及到这两个锁的操作。
void Hit Neighbor(node2_t *me) {
while (1) {
pthread_mutex_lock(&me->lock);
if (pthread_mutex_lock(&me->link->lock)!= 0) {
/* failed to get lock */
pthread_mutex_unlock(&me->lock);
continue;
}
break;
}
me->link->value += me->value;
me->value /=2;
pthread_mutex_unlock(&me->link->lock);
pthread_mutex_unlock(&me->lock);
}