使用互斥对象

互斥对象是互斥锁。 仅一个线程能持有锁。

互斥对象用于保护数据或者其他资源不被并发访问。 互斥对象具有属性,它指定互斥对象的特征。

互斥属性对象

类似于线程,互斥对象是在属性对象的帮助下创建的。 互斥属性对象是抽象对象,它包含多个属性,这取决于 POSIX 选项的实现。 它通过类型为 pthread_mutexattr_t的变量进行访问。 在AIX 中,pthread_mutexattr_t的数据类型是指针;在其他系统中,它可能是结构体或其他数据类型。

创建和破坏互斥属性对象

互斥属性对象由 pthread_mutexattr_init 子例程初始化为缺省值。 这些属性由子例程处理。 线程属性对象被 pthread_mutexattr_destroy 子例程破坏。 此子例程能释放 pthread_mutexattr_init 子例程动态分配的存储空间,这取决于线程库的实现。

在以下示例中,创建互斥属性对象并将其初始化为缺省值,然后使用该对象,最终删除它:
pthread_mutexattr_t attributes;
                /* the attributes object is created */
...
if (!pthread_mutexattr_init(&attributes)) {
                /* the attributes object is initialized */
        ...
                /* using the attributes object */
        ...
        pthread_mutexattr_destroy(&attributes);
                /* the attributes object is destroyed */
}

同一个属性对象能用来创建多个互斥对象。 在创建互斥对象之间也能修改它。 当创建互斥对象后,可以删除属性对象而不影响由它创建的互斥对象。

互斥属性

已定义以下互斥属性:

属性 描述
协议 指定用来防止互斥对象优先级反转的协议。 此属性取决于优先级继承或者优先级保护 POSIX 选项。
进程共享 指定互斥对象的进程共享。 此属性取决于进程共享 POSIX 选项。

有关这些属性的更多信息,请参阅 线程库选项同步调度

创建和破坏互斥对象

通过调用 pthread_mutex_init 子例程来创建互斥对象。 您可以指定互斥属性对象。 如果您指定 NULL 指针,互斥对象将会有缺省属性。 因而,以下代码片段:
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
...
pthread_mutexattr_init(&attr);
pthread_mutex_init(&mutex, &attr);
pthread_mutexattr_destroy(&attr);
等同于以下代码:
pthread_mutex_t mutex;
...
pthread_mutex_init(&mutex, NULL);

创建的互斥对象的标识通过 mutex 参数被返回给调用线程。 互斥标识是不透明对象; 其类型为 pthread_mutex_t。 在 AIX中, pthread_mutex_t 数据类型是结构; 在其他系统上,它可能是指针或其他数据类型。

互斥对象必须只被创建一次。 但是,避免使用相同的 mutex 参数多次调用 pthread_mutex_init 子例程 (例如,在同时执行相同代码的两个线程中)。 可以通过下面的方法确保创建互斥对象的唯一性:

  • 在创建将要使用此互斥对象的其他线程之前调用 pthread_mutex_init 子例程;例如,在初始线程中。
  • 在一次性初始化例程中调用 pthread_mutex_init 子例程。 有关更多信息,请参阅 一次性初始化
  • 使用由 PTHREAD_MUTEX_INITIALIZER 静态初始化宏初始化的静态互斥对象; 互斥对象将具有缺省属性。
不再需要互斥对象后,通过调用 pthread_mutex_destroy 子例程将其销毁。 此子例程可能回收 pthread_mutex_init 子例程分配的所有存储空间。 删除互斥对象之后,可再次使用同一个 pthread_mutex_t 变量创建另一个互斥对象。 例如,下面的代码片段是有效的(尽管不是十分实用):
pthread_mutex_t mutex;
...
for (i = 0; i < 10; i++) {
 
        /* creates a mutex */
        pthread_mutex_init(&mutex, NULL);
 
        /* uses the mutex */
 
        /* destroys the mutex */
        pthread_mutex_destroy(&mutex);
}

如同任何能够在线程间共享的系统资源,被分配到线程的堆栈上的互斥对象必须在线程终止之前被删除。 线程库维护互斥对象的已链接列表。 因此,如果被分配互斥对象的堆栈被释放,列表将被毁坏。

互斥对象的类型

互斥对象的类型确定对互斥对象执行操作时,该对象的行为。 存在以下类型的互斥对象:
PTHREAD_MUTEX_DEFAULTPTHREAD_MUTEX_NORMAL
如果相同的 pthread 试图不先解锁而使用 pthread_mutex_lock 子例程再次锁定它,将会造成死锁。 这是缺省类型。
pthread_mutex_errorcheck
如果相同的线程试图不先解锁互斥对象而再一次锁定同一个互斥对象,可通过返回非零值避免死锁。
pthread_mutex_recursive
允许同一个 pthread 使用 pthread_mutex_lock 子例程递归锁定互斥对象,而不造成死锁,或者从 pthread_mutex_lock 获取非零返回值。 同一个 pthread 调用 pthread_mutex_unlock 子例程的次数必须与其调用 pthread_mutex_lock 子例程的次数相同,以解锁互斥对象供其他 pthread 使用。

当互斥属性首先被创建时,其缺省类型为 PTHREAD_MUTEX_NORMAL。 在创建互斥对象之后,可以使用 pthread_mutexattr_settype API 库调用更改类型。

下面是创建并使用递归互斥类型的示例:
pthread_mutexattr_t    attr;
pthread_mutex_t         mutex;

pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &attr);

struct {
        int a;
        int b;
        int c;
} A;

f()
{
        pthread_mutex_lock(&mutex);
        A.a++;
        g();
        A.c = 0;
        pthread_mutex_unlock(&mutex);
}

g()
{
        pthread_mutex_lock(&mutex);
        A.b += A.a;
        pthread_mutex_unlock(&mutex);
}

锁定和解锁互斥对象

互斥对象是具有两种状态的简单锁:已锁定和已解锁。 当其创建时,互斥对象被解锁。 pthread_mutex_lock 子例程在下列情况下锁定指定的互斥对象:

  • 如果互斥对象被解锁,子例程则锁定它。
  • 如果互斥对象已经被另一个线程锁定,那么子例程会阻塞调用线程直到互斥对象被解锁。
  • 如果互斥对象已经被另一个调用线程锁定,子例程可能永远阻塞或者返回错误,这取决于互斥对象的类型。

pthread_mutex_trylock 子例程类似于 pthread_mutex_lock 子例程,在下列情况下不阻塞调用线程:

  • 如果互斥对象被解锁,子例程则锁定它。
  • 如果互斥对象已经被任何线程锁定,子例程返回错误。

锁定互斥对象的线程通常被称为互斥对象的所有者

pthread_mutex_unlock 子例程将指定的互斥对象重置为解锁状态 (如果在以下情况下由调用互斥对象拥有):

  • 如果互斥对象已经被解锁,子例程返回错误。
  • 如果互斥对象为调用线程所拥有,子例程解锁互斥对象。
  • 如果互斥对象为另一个线程所拥有,子例程可能返回错误或者解锁互斥对象,这取决于互斥的类型。 建议不要解锁互斥对象,因为互斥对象通常被同一个 pthread 锁定和解锁。

因为锁定不提供取消点,所以在等待互斥对象时被阻塞的线程不可以被取消。 因此,建议您仅在短时期内使用互斥对象,例如您正在保护数据不被并发访问的情况下。 有关更多信息,请参阅 取消点取消线程

使用互斥对象来保护数据

互斥对象旨在作为低级原语(可从其中构建其他线程同步函数)或者数据保护锁。 有关实现长锁和写程序优先级读程序/写程序锁的更多信息,请参阅 使用互斥对象

互斥对象用法示例

互斥对象可用于保护数据不被并发访问。 例如,数据库应用程序可以创建多个线程来并发处理多个请求。 数据库本身由称为 db_mutex 的互斥对象保护。 例如:
/* the initial thread */
pthread_mutex_t mutex;
int i;
...
pthread_mutex_init(&mutex, NULL);    /* creates the mutex      */
for (i = 0; i < num_req; i++)        /* loop to create threads */
        pthread_create(th + i, NULL, rtn, &mutex);
...                                  /* waits end of session   */
pthread_mutex_destroy(&mutex);       /* destroys the mutex     */
...

/* the request handling thread */
...                                  /* waits for a request  */
pthread_mutex_lock(&db_mutex);       /* locks the database   */
...                                  /* handles the request  */
pthread_mutex_unlock(&db_mutex);     /* unlocks the database */
...

初始线程创建互斥对象和所有请求处理线程。 通过使用线程入口点例程的参数将互斥对象传递给线程。 在实际程序中,互斥对象的地址可能是传递给已创建线程的更加复杂的数据结构的字段。

避免死锁

多线程应用程序在很多情况下会死锁。 下面是一些示例:
  • 使用缺省类型 (PTHREAD_MUTEX_NORMAL ) 创建的互斥对象不可能被同一个 pthread 重新锁定而不产生死锁。
  • 以相反的顺序锁定互斥对象时,应用程序会死锁。 例如,下面的代码片段在线程 A 和 B 之间产生死锁。
    /* Thread A */
    pthread_mutex_lock(&mutex1);
    pthread_mutex_lock(&mutex2);
    
    /* Thread B */
    pthread_mutex_lock(&mutex2);
    pthread_mutex_lock(&mutex1);
  • 应用程序会在资源死锁中死锁。 例如:
    struct {
                    pthread_mutex_t mutex;
                    char *buf;
            } A;
    
    struct {
                    pthread_mutex_t mutex;
                    char *buf;
            } B;
    
    struct {
                    pthread_mutex_t mutex;
                    char *buf;
            } C;
    
    use_all_buffers()
    {
            pthread_mutex_lock(&A.mutex);
            /* use buffer A */
    
            pthread_mutex_lock(&B.mutex);
            /* use buffers B */
    
            pthread_mutex_lock(&C.mutex);
            /* use buffer C */
    
            /* All done */
            pthread_mutex_unlock(&C.mutex);
            pthread_mutex_unlock(&B.mutex);
            pthread_mutex_unlock(&A.mutex);
    }
    
    use_buffer_a()
    {
            pthread_mutex_lock(&A.mutex);
            /* use buffer A */
            pthread_mutex_unlock(&A.mutex);
    }
    
    functionB()
    {
            pthread_mutex_lock(&B.mutex);
            /* use buffer B */
            if (..some condition)
            { 
              use_buffer_a();
            }
            pthread_mutex_unlock(&B.mutex);
    }
    
    /* Thread A */
    use_all_buffers();
    
    /* Thread B */
    functionB();
    此应用程序有两个线程: thread Athread BThread B 将首先开始运行,然后 thread A 将在此后不久启动。 如果 thread A 执行 use_all_buffers () 并成功锁定 A.mutex,那么当它尝试锁定 B.mutex时,它将阻塞,因为 thread B 已锁定它。 当 thread B 执行 functionB 并在 thread A 被阻止时发生 some_condition 时, thread B 现在还将阻止尝试获取 A.mutex(已被 thread A锁定)。 这将导致死锁。

    解决此死锁的方法是,每一个线程必须在使用资源之前获取所需的所有资源锁。 如果它无法获取锁,那么必须释放它们然后重新开始。

互斥对象和竞争状态

互斥锁(互斥对象)能防止竞争状态造成的数据不一致性。 当两个或者更多线程必须在同一个内存区域执行操作时会出现竞争状态,但是竞争的结果取决于执行操作的顺序。

例如,考虑由两个线程 A 和 B 递增的单个计数器 X。 如果 X 最初是 1 ,那么按线程 A 和 B 递增计数器的时间, X 应该是 3。 这两个线程都是独立的实体,它们之间没有同步。 虽然 C 语句X++看起来简单到原子,生成的组装代码可能不是,如以下伪汇编程序代码所示:
move    X, REG
inc     REG
move    REG, X

如果前面示例中的线程在两个 CPU 上被并发执行,或者如果通过调度使得线程交替执行每个指示信息,可能出现下面的步骤:

  1. 线程 A 执行第一条指令并将 X (为 1)放入线程 A 寄存器。 然后,线程 B 执行并将 X(为 1)放入线程 B 寄存器。 下面的示例说明了寄存器结果和内存 X 的内容。
    Thread A Register = 1
    Thread B Register = 1
    Memory X          = 1
  2. 线程 A 执行第二条指令并将其寄存器的内容递增到 2。 然后,线程 B 将其寄存器递增到 2。 不会将任何内容移至内存 X,因此内存 X 保持不变。 下面的示例说明了寄存器结果和内存 X 的内容。
    Thread A Register = 2
    Thread B Register = 2
    Memory X          = 1
  3. 线程 A 将其寄存器 (现在为 2) 的内容移动到内存 X中。 然后,线程 B 将其寄存器 (也为 2) 的内容移动到内存 X中,从而覆盖线程 A 的值。 下面的示例说明了寄存器结果和内存 X 的内容。
    Thread A Register = 2
    Thread B Register = 2
    Memory X          = 2

在大多数情况下,线程 A 和线程 B 依次执行三条指令,预期的结果将为 3。 很难发现竞争状态,因为它们间歇出现。

为了避免这种竞争情况,每个线程都应该在访问计数器和更新内存 X之前锁定数据。 例如,如果线程 A 获取锁定并更新计数器,那么它会使内存 X 的值为 2。 线程 A 释放锁定后,线程 B 将获取锁定并更新计数器,将 2 作为其 X 的初始值,并将其递增到预期结果 3。