9. POSIX互斥锁

POSIX互斥锁(Mutex)是POSIX线程库提供的 独占式同步工具 ,核心用于解决多线程/多进程场景下临界资源的互斥访问问题, 是并发编程中最常用的锁机制。

9.1. 互斥锁基本概念

互斥锁即互斥量,是一种二值独占锁,状态仅有开锁(未持有)和闭锁(已持有)两种,同一时刻仅允许一个执行流(线程/亲缘进程)持有, 以此实现临界资源的独占式访问,从根源避免多执行流并发操作引发的数据竞态、逻辑错乱问题。

相较于POSIX信号量,互斥锁具备所有权归属、递归访问、类型可控三大核心特性,这也是二者的本质区别:

  • 所有权独占:互斥锁遵循“谁加锁、谁解锁”原则,只有持有锁的执行流才能释放锁,禁止跨执行流解锁,杜绝信号量“任意执行流释放”导致的互斥失效问题;

  • 递归访问:支持同一执行流重复加锁,仅需对应次数解锁即可,避免递归调用、嵌套逻辑引发的自死锁;

  • 状态严格:无计数功能,仅做独占互斥,逻辑更单一,同步开销更低,适配纯互斥场景。

信号量兼具互斥与同步双重功能,支持计数限流,可跨无亲缘进程使用; 互斥锁专注独占互斥,安全性更高,更适合保护临界资源,同步场景优先选用信号量或条件变量。

9.1.1. 死锁成因与规避规则

死锁是并发锁编程的核心隐患,指执行流因锁竞争陷入永久阻塞的状态,互斥锁场景下主要分为两类:

  • 自死锁:同一执行流重复加锁非递归互斥锁,因锁已被占用陷入自我阻塞,是最常见的死锁场景;

  • 交叉死锁:多个执行流循环持有对方所需的锁,相互等待释放,形成阻塞闭环。

规避死锁需遵循刚性规范,从编码层面杜绝隐患:

  1. 加锁后必须解锁,杜绝临界区异常退出导致锁泄露;

  2. 尽量缩短锁持有时间,临界区仅保留资源操作逻辑,减少阻塞时长;

  3. 多锁场景严格遵循统一加锁顺序、逆序释放,禁止交叉加锁;

  4. 优先选用递归互斥锁处理嵌套加锁场景,避免自死锁;

  5. 非必要不使用阻塞加锁,可搭配非阻塞加锁做超时重试。

9.2. 互斥锁应用场景

  • 保护全局变量、共享内存、硬件外设等独占式临界资源;

  • 线程递归调用、嵌套逻辑中需重复加锁的场景;

  • 追求高安全性,需严格管控锁所有权的纯互斥场景;

  • 降低优先级翻转影响,适配实时性要求较高的线程场景。

9.3. 互斥锁初始化

POSIX互斥锁支持静态初始化和动态初始化两种方式, 初始化前需定义pthread_mutex_t类型的锁变量,且只能初始化一次,禁止重复初始化。

9.3.1. 静态初始化

在使用互斥锁前需要初始化一个互斥锁,而在POSIX标准中支持互斥锁静态初始化和动态初始化两种方式, 如果是静态初始化的可以通过以下代码实现(选择其中一句即可):

1
2
3
4
5
6
7
#include <pthread.h>

pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;

pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;

pthread_mutex_t是互斥锁的结构体,其实就是定义一个互斥锁结构,并且将其赋值,代表不同的互斥锁, 这3种锁的区别主要在于其他未占有互斥锁的线程在获取互斥锁时是否需要阻塞等待:

  • PTHREAD_MUTEX_INITIALIZER:表示默认的互斥锁,即快速互斥锁。互斥锁被线程1持有时,此时互斥锁处于闭锁状态, 当线程2尝试获取互斥锁,那么线程2将会阻塞直至持有互斥锁的线程1解锁为止。

  • PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP:递归互斥锁。互斥锁被线程1持有时,线程2尝试获取互斥锁, 将无法获取成功,并且阻塞等待,而如果是线程1尝试再次获取互斥锁时,将获取成功,并且持有互斥锁的次数加1。

  • PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP:检错互斥锁。这是快速互斥锁的非阻塞版本,它会立即返回一个错误代码(线程不会阻塞)。

9.3.2. 动态初始化

互斥锁动态初始化可以调用pthread_mutex_init()函数,该函数原型如下:

1
2
3
#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);

pthread_mutex_init()函数是以动态方式初始化互斥锁的,参数说明如下:

  • mutex则是初始化互斥锁结构的指针,

  • mutexattr是属性参数,它允许我们设置互斥锁的属性,从而属性控制着互斥锁的行为,如果参数mutexattr为NULL, 则使用默认的互斥锁属性,默认属性为快速互斥锁。

9.4. 互斥锁函数说明

互斥锁操作遵循加锁->操作临界资源->解锁的标准流程,所有API执行成功返回0,失败返回对应错误码。

9.4.1. 获取互斥锁与释放互斥锁

当互斥锁处于开锁状态时,线程才能够获取互斥锁,当线程持有了某个互斥锁的时候, 其他线程就无法获取这个互斥锁,需要等到持有互斥锁的线程进行释放后,其他线程才能获取成功, 线程通过互斥锁获取函数来获取互斥锁的所有权。

获取互斥锁有2个函数,mutex参数指定了要操作的互斥锁:

1
2
3
4
5
6
7
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • pthread_mutex_lock()函数获得访问临界资源的权限,如果已经有其他线程锁住互斥锁,那么该函数会是线程阻塞指定该互斥锁解锁为止。

  • pthread_mutex_trylock()是pthread_mutex_lock()函数的非阻塞版本,使用它不会阻塞当前线程,如果互斥锁已被占用, 它会理解返回一个EBUSY错误。

  • 访问完共享资源后,一定要通过pthread_mutex_unlock()函数释放占用的互斥锁,以便系统其他线程有机会获取互斥锁,访问该资源。

线程对互斥锁的所有权是独占的,任意时刻互斥锁只能被一个线程持有,如果互斥锁处于开锁状态, 那么获取该互斥锁的线程将成功获得该互斥锁,并拥有互斥锁的所有权; 而如果互斥锁处于闭锁状态,则根据互斥锁的类型做对应的处理,默认情况下是快速互斥锁, 获取该互斥锁的线程将无法获得互斥锁,线程将被阻塞,直到互斥锁被释放,当然,如果是同一个线程重复获取互斥锁,也会导致死锁结果。

9.4.2. 销毁互斥锁

pthread_mutex_destroy函数用于销毁一个互斥锁,当互斥锁不再使用时,可以用它来销毁,mutex参数指定了要销毁的互斥锁:

1
2
3
#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);

需注意:

  • 静态初始化的锁无需销毁;

  • 销毁前需确保锁处于开锁状态,无执行流阻塞等待;

  • 销毁后的锁禁止再次操作。

9.5. 互斥锁示例

这个实验主要是验证互斥锁的互斥情况,系统创建3个线程,假设这3个线程中有临界资源被访问, 那么我们希望这3个线程按顺序且不能同时去访问这个临界资源(假设临界资源是调用sleep()函数), 所以我们可以使用互斥锁去限制能访问的线程,获取到互斥锁的线程可以访问临界资源。

代码如下:

POSIX无名信号量(base_code/system_programing/mutex/sources/mutex.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define THREAD_NUMBER 3 /* 线程数 */

pthread_mutex_t mutex;

void *thread_func(void *arg)
{
    int num = (unsigned long long)arg; /** sizeof(void*) == 8 and sizeof(int) == 4 (64 bits) */
    int sleep_time = 0;
    int res;

    /* 互斥锁上锁 */
    res = pthread_mutex_lock(&mutex);
    if (res)
    {   /*获取失败*/
        printf("Thread %d lock failed\n", num);

        /* 互斥锁解锁 */
        pthread_mutex_unlock(&mutex);

        pthread_exit(NULL);
    }

    printf("Thread %d is hold mutex\n", num);

    /*睡眠一定时间*/
    sleep(2);

    printf("Thread %d freed mutex\n\n", num);

    /* 互斥锁解锁 */
    pthread_mutex_unlock(&mutex);

    pthread_exit(NULL);
}


int main(void)
{
    pthread_t thread[THREAD_NUMBER];
    int num = 0, res;

    srand(time(NULL));

    /* 互斥锁初始化 */
    pthread_mutex_init(&mutex, NULL);
    for (num = 0; num < THREAD_NUMBER; num++)
    {
        /*创建线程*/
        res = pthread_create(&thread[num], NULL, thread_func, (void*)(unsigned long long)num);
        if (res != 0)
        {
            printf("Create thread %d failed\n", num);
            exit(res);
        }
    }

    for (num = 0; num < THREAD_NUMBER; num++)
    {
        /*等待线程结束*/
        pthread_join(thread[num], NULL);
    }

    /*销毁互斥锁*/
    pthread_mutex_destroy(&mutex);

    return 0;
}

本代码说明如下,直接从main函数按流程分析:

  • 第45行,调用pthread_mutex_init()动态初始化了一个互斥锁。

  • 第54行,在for循环内创建三个线程,线程要调用的函数均为thread_func,并且通过变量num传入了线程序号。

  • 第11行,三个线程执行的都是这同样的函数。

    • 第18行,调用pthread_mutex_lock()等待互斥锁,若互斥锁被其它线程占用,线程将阻塞在此处等待。

    • 第29、34行,这两行输出信息的代码均处在互斥锁保护的范围内,所以它们是成对出现,而且不会被需求同样互斥锁的其它线程打断。

    • 第32行,获取到互斥锁后对资源进行操作,此处直接使用sleep模拟,并且释放CPU占用。

    • 第37行,调用pthread_mutex_unlock()解锁,以便其它线程使用临界资源。

  • 第65行,此时各个子线程已经创建完成,并且执行,这时在原线程里的for循环里调用pthread_join()等待其它线程执行完毕。

  • 第69行,各个线程已执行完毕调用pthread_mutex_destroy()销毁互斥锁。

可以推算到如下现象: 在原线程的控制下,它创建的线程ABC,在互斥锁的保护下输出代码里成对的29、34行中的信息, 而且即使保护范围内有释放CPU的sleep操作,另外两个线程也不会得到CPU的光顾,因为它们未得到锁。 从而在控制下出现的输出信息是:即A1A2,B1B2,C1C2,而不会出现 A1B1C1,A2B2C2 之类的打乱信息。

9.5.1. 实验操作

本实验的代码存储在 base_code/system_programing/mutex 目录中,编译及运行过程如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 以下操作在 base_code/system_programing/mutex 代码目录进行
make

# 运行
./build/mutex_demo

# 以下是运行的输出
Thread 0 is hold mutex
Thread 0 freed mutex

Thread 1 is hold mutex
Thread 1 freed mutex

Thread 2 is hold mutex
Thread 2 freed mutex

如果觉得不太明确的话,可以将互斥锁的相关代码注释掉,即注释掉18~27行代码, 看看如果没有互斥操作的话线程会怎样访问临界资源,再重新编译运行,运行结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 注释掉18~27行代码后重新编译运行
# 以下操作在 base_code/system_programing/mutex 代码目录进行
# 编译
make

# 运行
./build_x86/mutex_demo

# 注释掉18~27行代码后重新编译运行后的输出

Thread 0 is hold mutex
Thread 1 is hold mutex
Thread 2 is hold mutex
Thread 0 freed mutex

Thread 2 freed mutex

Thread 1 freed mutex

可以看到,由于没有锁的保护,在sleep期间其它线程得到了CPU,也对资源进行了操作, 此处出现了A1C1B1,A2C2B2的信息打印顺序。就表示了线程A还在使用关键资源的时候, 线程B和C也同时使用了,如果资源只有一个,那就会出现不可预知的问题。