Linux多线程同步机制-互斥锁(mutex)

系统 Linux
在使用互斥锁时,需要注意正确的加锁和解锁顺序,避免死锁等问题。同时,过度使用互斥锁可能导致性能下降,因为线程可能会因等待锁而阻塞。因此,在设计多线程程序时,需要仔细考虑互斥锁的使用位置和时机,以达到最佳的性能和正确性平衡。

引言

在Linux多线程编程中,互斥锁(Mutex)是一种非常重要的同步机制,用于控制对共享资源的访问,确保在任意时刻只有一个线程可以访问特定的资源或代码段,即临界区。互斥锁的主要用途是防止多个线程同时访问共享资源,从而避免竞争条件和数据不一致的问题。

互斥锁的工作原理相对简单,它通过锁定和解锁操作来控制对共享资源的访问。当一个线程需要访问共享资源时,它首先尝试锁定互斥锁。如果互斥锁已经被其他线程锁定,请求线程将被阻塞,直到互斥锁被解锁。互斥锁的锁定和解锁操作必须是成对出现的,以确保对共享资源的正确访问。

一、互斥锁原型

在 Linux 中,互斥锁通常通过 POSIX 线程库(pthread)来实现。pthread 库提供了一系列的函数来创建、初始化、锁定、解锁和销毁互斥锁,如pthread_mutex_init()、pthread_mutex_lock()、pthread_mutex_unlock()和pthread_mutex_destroy()等。互斥锁的初始化是通过pthread_mutex_init()函数完成的,该函数会分配必要的资源来创建一个互斥锁,并将其初始化为未锁定状态。

pthread_mutex_t 是 POSIX 线程库中用于互斥锁的数据类型。以下是一些与 pthread_mutex_t 相关的函数原型:

初始化互斥锁:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

mutex:指向 pthread_mutex_t 结构的指针,用于初始化互斥锁。

attr:指向 pthread_mutexattr_t 结构的指针,包含互斥锁的属性。如果为 NULL,则使用默认属性。

销毁互斥锁:

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • mutex:指向已经初始化的 pthread_mutex_t 结构的指针。销毁互斥锁前,确保互斥锁没有被锁定。

锁定互斥锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • mutex:指向已经初始化的 pthread_mutex_t 结构的指针。如果互斥锁已经被其他线程锁定,则调用线程将被阻塞,直到互斥锁被解锁。

尝试锁定互斥锁(非阻塞):

int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • mutex:指向已经初始化的 pthread_mutex_t 结构的指针。如果互斥锁已经被锁定,则立即返回错误码 EBUSY,而不是等待互斥锁变为可用。

定时锁定互斥锁(可指定超时时间):

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
  • mutex:指向已经初始化的 pthread_mutex_t 结构的指针。
  • abstime:指向 timespec 结构的指针,包含超时时间。如果超时时间到达,互斥锁仍未解锁,则返回 ETIMEDOUT。

解锁互斥锁:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • mutex:指向已经初始化且当前被调用线程锁定的 pthread_mutex_t 结构的指针。调用此函数将释放互斥锁,允许其他线程锁定它。

注意:互斥锁的初始化方式主要有两种:静态初始化和动态初始化。

  • 静态初始化: 使用宏 PTHREAD_MUTEX_INITIALIZER 可以在声明互斥锁变量时直接初始化。这种方式是编译时初始化,无需调用初始化函数。例如:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

这种方式适用于静态分配的互斥锁,即在程序的整个生命周期内都存在的锁。

  • 动态初始化: 使用 pthread_mutex_init() 函数可以在程序运行时初始化互斥锁。这种方式需要显式调用函数进行初始化和销毁,适用于需要动态创建和销毁的互斥锁。例如:

pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));pthread_mutex_init(mutex, NULL); // NULL 表示使用默认属性

使用动态初始化时,需要在不再需要互斥锁时调用 pthread_mutex_destroy() 来销毁它,并释放分配的内存。

互斥锁的属性可以在初始化时指定,如果不设置属性(即使用 NULL 或默认属性),则使用系统默认的互斥锁属性。在Linux系统中,属性可以用来定义不同类型的互斥锁,例如:

  • 普通互斥锁(PTHREAD_MUTEX_TIMED_NP)
  • 递归互斥锁(PTHREAD_MUTEX_RECURSIVE_NP)
  • 错误检查互斥锁(PTHREAD_MUTEX_ERRORCHECK_NP)
  • 适应性互斥锁(PTHREAD_MUTEX_ADAPTIVE_NP)

正确初始化互斥锁对于避免潜在的同步问题是非常重要的。使用静态初始化可以简化代码并减少出错机会,而动态初始化提供了更大的灵活性。在实际编程中,应根据具体需求选择适合的初始化方式。

这些函数是 POSIX 线程库中用于线程同步的基础,通过它们可以安全地在多线程程序中控制对共享资源的访问。

三、互斥锁特性

互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁(unlock),如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

互斥锁特点如下:

  1. 原子性:把一个互斥量锁定为一个原子操作,操作系统保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以再成功锁定这个互斥量;
  2. 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;
  3. 非忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何 cpu 资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。

四、互斥锁加锁阻塞

互斥锁是一种【独占锁】,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞。

对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为【睡眠】状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。如下图:

图片图片

所以,互斥锁加锁失败时,会从用户态陷入到内核态,让内核帮我们切换线程,虽然简化了使用锁的难度,但是存在一定的性能开销成本。

那这个开销成本是什么呢?会有两次线程上下文切换的成本:

  • 当线程加锁失败时,内核会把线程的状态从【运行】状态设置为【睡眠】状态,然后把 CPU 切换给其他线程运行;
  • 接着,当锁被释放时,之前【睡眠】状态的线程会变为【就绪】状态,然后内核会在合适的时间,把 CPU 切换给该线程运行。

线程的上下文切换的是什么?当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。

上下切换的耗时有大佬统计过,大概在几十纳秒到几微秒之间,如果你锁住的代码执行时间比较短,那可能上下文切换的时间都比你锁住的代码执行时间还要长。

所以,如果你能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁。

五、互斥锁死锁

互斥锁导致的死锁是多线程编程中的一个常见问题。死锁发生时,两个或多个线程被无限期地阻塞,因为它们在等待对方释放锁。以下是一些关于互斥锁死锁的信息:

  1. 死锁的条件:死锁通常发生在以下四个条件同时满足时:

互斥:资源不能被多个线程同时访问。

占有和等待:一个线程持有至少一个锁,并且等待获取其他线程持有的锁。

不可剥夺:已经获得的锁不能被其他线程强行剥夺,只能由持有它的线程释放。

循环等待:存在一个线程持有锁的循环链,每个线程都在等待下一个线程持有的锁。

  1. 死锁的避免:可以通过以下几种策略来避免死锁:

固定顺序加锁:总是以相同的顺序获取多个锁。

超时尝试加锁:使用 pthread_mutex_trylock() 或其他带有超时功能的加锁方法。

一次性获取所有锁:在开始访问共享资源前,先获取所有需要的锁。

使用死锁检测算法:定期检测死锁情况,并采取措施解决。

  1. 死锁的解除:如果检测到死锁,可以采取以下措施:

剥夺资源:从其他线程剥夺资源,赋予死锁线程。

撤销进程:终止一个或多个线程或进程,打破死锁状态。

资源重分配:重新分配资源,以打破循环等待条件。

  1. 避免嵌套锁:尽量减少锁的嵌套使用,如果必须嵌套使用,确保内层锁总是不同类型的或者使用不同的加锁顺序。
  2. 锁的分级管理:将锁分级,高级别的锁可以包含多个低级别的锁,确保在请求高级别锁时,已经持有所有需要的低级别锁。

通过采取这些措施,可以降低死锁发生的风险,并提高多线程程序的稳定性和可靠性。

六、互斥锁实战

1.互斥锁加解锁

以下是一个简单的示例代码,展示了如何在多线程环境中使用 pthread_mutex_t 来同步对同一个文件的读写操作:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define NUM_THREADS 5 // 定义宏来设置线程数量

pthread_mutex_t file_mutex = PTHREAD_MUTEX_INITIALIZER; // 全局互斥锁

const char *data_to_write = "Thread data\n";

void perform_file_write(const char *filename) {
    FILE *fp = fopen(filename, "a");
    if (fp == NULL) {
        perror("Error opening file for write");
        return;
    }

    pthread_mutex_lock(&file_mutex); // 加锁
    fputs(data_to_write, fp);
    pthread_mutex_unlock(&file_mutex); // 解锁
    fclose(fp);
}

void perform_file_read(const char *filename) {
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
        perror("Error opening file for read");
        return;
    }

    char buffer[256];
    pthread_mutex_lock(&file_mutex); // 加锁
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("%s", buffer); // 打印读取的数据
    }
    pthread_mutex_unlock(&file_mutex); // 解锁
    fclose(fp);
}

void *thread_function(void *arg) {
    int is_write = *(int *)arg; // 根据传入的参数决定操作类型
    const char *filename = "example.txt"; // 要操作的文件名

    if (is_write) {
        perform_file_write(filename);
    } else {
        perform_file_read(filename);
    }
    return NULL;
}

int main() {
    int *args = malloc(NUM_THREADS * sizeof(int)); // 动态分配参数数组
    if (args == NULL) {
        perror("Failed to allocate memory for args");
        return 1;
    }

    pthread_t threads[NUM_THREADS];
    for (int i = 0; i < NUM_THREADS; ++i) {
        args[i] = (i == 0); // 第一个线程执行写操作,其余执行读操作
        if (pthread_create(&threads[i], NULL, thread_function, &args[i]) != 0) {
            perror("Failed to create thread");
            free(args);
            return 1;
        }
    }

    for (int i = 0; i < NUM_THREADS; ++i) {
        pthread_join(threads[i], NULL);
    }

    free(args); // 释放参数数组
    // 静态初始化的互斥锁不需要显式销毁
    return 0;
}

在这个示例中,创建了一个互斥锁 file_mutex 来同步对文件 example.txt 的访问。使用 PTHREAD_MUTEX_INITIALIZER 直接初始化互斥锁,无需再调用pthread_mutex_init 函数了。我们定义了 perform_file_read 和 perform_file_write 函数来执行实际的文件读写操作,并在这些操作前后使用 pthread_mutex_lock 和 pthread_mutex_unlock 来确保每次只有一个线程可以访问文件。在 main 函数中,创建了一个线程数组,并设置第一个线程执行写操作,其余线程执行读操作。使用 perror 来打印出创建线程或文件操作失败时的错误信息。最后,我们在主函数中等待所有线程完成,并且由于使用了静态初始化互斥锁,我们不需要调用 pthread_mutex_destroy 来销毁互斥锁。

程序运行结果如下:

[root@localhost multi_pthread_file]# ./multi_pthread_file_rw
Thread data
Thread data
Thread data
Thread data
[root@localhost multi_pthread_file]# cat example.txt
Thread data

通过程序运行结果可知,我们通过给写操作和读操作加互斥锁,成功实现我们预期的目标,即写一次数据,读四次数据。

2.互斥锁死锁

以下是一个 C 语言中可能导致死锁的互斥锁代码示例:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex1;
pthread_mutex_t mutex2;

void *thread1Function(void *arg) {
    pthread_mutex_lock(&mutex1);
    printf("Thread 1 acquired mutex1\n");

    sleep(1);

    pthread_mutex_lock(&mutex2);
    printf("Thread 1 acquired mutex2\n");

    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);

    return NULL;
}

void *thread2Function(void *arg) {
    pthread_mutex_lock(&mutex2);
    printf("Thread 2 acquired mutex2\n");

    sleep(1);

    pthread_mutex_lock(&mutex1);
    printf("Thread 2 acquired mutex1\n");

    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);

    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    pthread_mutex_init(&mutex1, NULL);
    pthread_mutex_init(&mutex2, NULL);

    pthread_create(&thread1, NULL, thread1Function, NULL);
    pthread_create(&thread2, NULL, thread2Function, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    pthread_mutex_destroy(&mutex1);
    pthread_mutex_destroy(&mutex2);

    return 0;
}

在这个示例中,thread1 先获取 mutex1 然后尝试获取 mutex2,而 thread2 先获取 mutex2 然后尝试获取 mutex1,可能会导致两个线程相互等待对方释放锁,从而造成死锁。

【拓展】上文中 sleep(1) 的作用:

在上述代码中,两个线程函数中设置 sleep(1) 的主要目的是增加死锁发生的可能性。当线程获取一个互斥锁后,通过 sleep(1) 让线程暂停一段时间,使得另一个线程有机会去获取另一个互斥锁,从而更有可能形成两个线程相互等待对方持有的锁的情况,导致死锁的发生。

如果没有这个 sleep(1) ,由于线程执行速度非常快,可能在一个线程完成对两个锁的获取和操作之前,另一个线程还没有机会执行获取锁的操作,这样死锁就不太容易出现,不利于演示和观察死锁的情况。

通过添加 sleep(1) ,模拟了线程操作中的一定延迟,使得线程之间的竞争和等待更加明显,更有可能展示出死锁的现象。

编译并执行可执行程序如下:

[root@localhost multi_pthread_file]# ./dead_lock
Thread 1 acquired mutex1
Thread 2 acquired mutex2

执行可执行程序 dead_lock ,发现该进程在输出两条信息后卡住不动,无法继续执行,出现停滞或长时间无响应的情况,由此推断该进程死锁了。

使用 gdb 命令附加到可能发生死锁的进程。可以通过 ps 命令找到进程 ID(PID),然后使用 gdb 加上进程 ID 来附加到该进程。例如:

[root@localhost multi_pthread_file]# gdb -p 251640 -q
Attaching to process 251640
[New LWP 251641]
[New LWP 251642]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
0x00007fb09e6906cd in __pthread_timedjoin_ex () from /lib64/libpthread.so.0
Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-251.el8.x86_64
(gdb) info threads
  Id   Target Id                                      Frame
* 1    Thread 0x7fb09eac8740 (LWP 251640) "dead_lock" 0x00007fb09e6906cd in __pthread_timedjoin_ex () from /lib64/libpthread.so.0
  2    Thread 0x7fb09e2b0700 (LWP 251641) "dead_lock" 0x00007fb09e69885d in __lll_lock_wait () from /lib64/libpthread.so.0
  3    Thread 0x7fb09daaf700 (LWP 251642) "dead_lock" 0x00007fb09e69885d in __lll_lock_wait () from /lib64/libpthread.so.0
(gdb)

使用 info threads 命令列出进程中的所有线程。这将显示每个线程的 ID 和当前状态。死锁的线程通常会显示为在等待锁(例如,在 __lll_lock_wait)。根据上述结果可知,该进程已死锁。

3互斥锁死锁检测和恢复

以下是一个使用 C 语言实现互斥锁、死锁检测和恢复的简单示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>

pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;

int lock1_held = 0;
int lock2_held = 0;

void *thread1(void *arg) {
    pthread_mutex_lock(&lock1);
    printf("Thread 1 acquired lock 1\n");
    lock1_held = 1;

    sleep(1);  // 模拟耗时操作

    while (lock2_held && lock1_held) {  // 持续检测死锁条件
        printf("Thread 1 detects potential deadlock and releases lock 1\n");
        pthread_mutex_unlock(&lock1);
        lock1_held = 0;
        sleep(1);  // 等待一段时间后再次尝试
        pthread_mutex_lock(&lock1);
        lock1_held = 1;
    }

    pthread_mutex_lock(&lock2);
    printf("Thread 1 acquired lock 2\n");

    pthread_mutex_unlock(&lock2);
    pthread_mutex_unlock(&lock1);
    lock1_held = 0;
    lock2_held = 0;

    return NULL;
}

void *thread2(void *arg) {
    pthread_mutex_lock(&lock2);
    printf("Thread 2 acquired lock 2\n");
    lock2_held = 1;

    sleep(1);  // 模拟耗时操作

    while (lock1_held && lock2_held) {  // 持续检测死锁条件
        printf("Thread 2 detects potential deadlock and releases lock 2\n");
        pthread_mutex_unlock(&lock2);
        lock2_held = 0;
        sleep(1);  // 等待一段时间后再次尝试
        pthread_mutex_lock(&lock2);
        lock2_held = 1;
    }

    pthread_mutex_lock(&lock1);
    printf("Thread 2 acquired lock 1\n");

    pthread_mutex_unlock(&lock1);
    pthread_mutex_unlock(&lock2);
    lock1_held = 0;
    lock2_held = 0;

    return NULL;
}

int main() {
    pthread_t t1, t2;

    pthread_create(&t1, NULL, thread1, NULL);
    pthread_create(&t2, NULL, thread2, NULL);

    sleep(5);  // 等待一段时间,让死锁有机会发生

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    return 0;
}

在上述示例中,我们进行了如下操作:

1)定义了两个互斥锁 lock1 和 lock2,以及两个标志 lock1_held 和 lock2_held 来跟踪锁的持有状态。

2)thread1 函数:

  • 首先获取 lock1 并设置相应的标志。
  • 经过一段模拟耗时操作后,进入一个 while 循环,持续检测是否同时持有 lock1 和 lock2 导致死锁。
  • 如果检测到死锁,打印提示信息,释放 lock1,将标志重置,等待一段时间后重新获取 lock1 并再次设置标志。
  • 如果没有死锁,获取 lock2,完成操作后释放两个锁并重置标志。

3)thread2 函数:

  • 与 thread1 函数类似,首先获取 lock2 并设置标志。
  • 经过模拟耗时操作后,在 while 循环中检测死锁情况并进行相应处理。
  • 最终获取 lock1,完成操作后释放锁和重置标志。

4)main 函数:

  • 创建两个线程分别执行 thread1 和 thread2 函数。
  • 等待一段时间,让死锁有机会发生。
  • 等待两个线程结束。
[root@localhost multi_pthread_file]# ./dead_lock_detect
Thread 1 acquired lock 1
Thread 2 acquired lock 2
Thread 1 detects potential deadlock and releases lock 1
Thread 2 detects potential deadlock and releases lock 2
Thread 1 acquired lock 2
Thread 2 acquired lock 1

通过这种方式,每个线程在获取第二个锁之前,持续检测死锁情况,并在可能死锁时采取释放已持有的锁、等待后重新尝试获取的策略,以尽量避免死锁的发生。但需要注意的是,这仍然不是一种完全可靠的死锁避免机制,在复杂的多线程环境中,可能需要更完善的同步和协调策略。

4.固定顺序加锁避免死锁

以下是一个使用 C 语言实现互斥锁通过固定加锁顺序来避免死锁的发生简单示例代码:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 互斥锁
pthread_mutex_t mutex1;
pthread_mutex_t mutex2;

// 死锁检测标志
int deadlockDetected = 0;

void *thread1Function(void *arg) {
    pthread_mutex_lock(&mutex1);
    printf("Thread 1 acquired mutex1\n");

    sleep(1);

    pthread_mutex_lock(&mutex2);
    printf("Thread 1 acquired mutex2\n");

    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);

    return NULL;
}

void *thread2Function(void *arg) {
    pthread_mutex_lock(&mutex1);
    printf("Thread 2 acquired mutex1\n");

    sleep(1);

    pthread_mutex_lock(&mutex2);
    printf("Thread 2 acquired mutex2\n");

    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);

    return NULL;
}

// 模拟死锁检测
void detectDeadlock() {
    sleep(3);
    // 由于固定了加锁顺序,这里不会发生死锁,无需检测
}

int main() {
    pthread_t thread1, thread2;

    pthread_mutex_init(&mutex1, NULL);
    pthread_mutex_init(&mutex2, NULL);

    pthread_create(&thread1, NULL, thread1Function, NULL);
    pthread_create(&thread2, NULL, thread2Function, NULL);

    detectDeadlock();

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    pthread_mutex_destroy(&mutex1);
    pthread_mutex_destroy(&mutex2);

    return 0;
}

在上述示例中,我们进行了如下操作:

  1. 首先,定义了两个互斥锁 mutex1 和 mutex2,以及一个标志 deadlockDetected 用于死锁检测(但在当前代码中未实际使用)。
  2. thread1Function 和 thread2Function 分别是两个线程的执行函数。

thread1Function 先获取 mutex1,等待 1 秒后再获取 mutex2,最后释放两个锁。

thread2Function 逻辑相同,也是先获取 mutex1,等待 1 秒后获取 mutex2,最后释放。

  1. detectDeadlock 函数用于模拟死锁检测,但由于当前固定了加锁顺序,实际上不会发生死锁,所以此函数内未进行真正的检测操作。
  2. 在 main 函数中:

初始化两个互斥锁。

创建两个线程分别执行 thread1Function 和 thread2Function 。

调用 detectDeadlock 函数进行死锁检测(但如前所述,此处在当前代码中未实际生效)。

使用 pthread_join 等待两个线程结束。

最后销毁两个互斥锁。 总的来说,这段代码创建了两个线程并尝试获取两个互斥锁,但由于固定的加锁顺序,不会产生死锁。

程序运行结果如下:

[root@localhost multi_pthread_file]# ./dead_lock_detect_fixed_mutex
Thread 1 acquired mutex1
Thread 1 acquired mutex2
Thread 2 acquired mutex1
Thread 2 acquired mutex2

通过程序运行结果可知,多个线程使用固定的加锁顺序,不会产生死锁。

5.pthread_mutex_trylock()函数避免死锁

以下是一个使用 C 语言实现互斥锁通过 pthread_mutex_trylock() 来避免死锁的发生简单示例代码:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

pthread_mutex_t lock1, lock2;

void *thread1(void *arg) {
    while (1) {
        // 循环尝试获取锁 1
        if (pthread_mutex_trylock(&lock1) == 0) {
            printf("Thread 1: Acquired lock 1\n");
            break;
        }
        printf("Thread 1: Failed to acquire lock 1, retrying...\n");
        sleep(1);  // 等待一段时间再重试
    }

    // 模拟一些操作
    sleep(1);

    while (1) {
        // 循环尝试获取锁 2
        if (pthread_mutex_trylock(&lock2) == 0) {
            printf("Thread 1: Acquired lock 2\n");
            break;
        }
        printf("Thread 1: Failed to acquire lock 2, retrying...\n");
        pthread_mutex_unlock(&lock1);  // 释放锁 1
        sleep(1);  // 等待一段时间再重试
    }

    // 释放锁 2 和锁 1
    pthread_mutex_unlock(&lock2);
    pthread_mutex_unlock(&lock1);

    return NULL;
}

void *thread2(void *arg) {
    while (1) {
        // 循环尝试获取锁 2
        if (pthread_mutex_trylock(&lock2) == 0) {
            printf("Thread 2: Acquired lock 2\n");
            break;
        }
        printf("Thread 2: Failed to acquire lock 2, retrying...\n");
        sleep(1);  // 等待一段时间再重试
    }

    // 模拟一些操作
    sleep(1);

    while (1) {
        // 循环尝试获取锁 1
        if (pthread_mutex_trylock(&lock1) == 0) {
            printf("Thread 2: Acquired lock 1\n");
            break;
        }
        printf("Thread 2: Failed to acquire lock 1, retrying...\n");
        pthread_mutex_unlock(&lock2);  // 释放锁 2
        sleep(1);  // 等待一段时间再重试
    }

    // 释放锁 1 和锁 2
    pthread_mutex_unlock(&lock1);
    pthread_mutex_unlock(&lock2);

    return NULL;
}

int main() {
    pthread_t t1, t2;

    // 初始化锁
    pthread_mutex_init(&lock1, NULL);
    pthread_mutex_init(&lock2, NULL);

    // 创建线程
    pthread_create(&t1, NULL, thread1, NULL);
    pthread_create(&t2, NULL, thread2, NULL);

    // 等待线程结束
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    // 销毁锁
    pthread_mutex_destroy(&lock1);
    pthread_mutex_destroy(&lock2);

    return 0;
}

在上述示例中,我们进行了如下操作:

  1. 首先,在程序中定义了两个互斥锁 lock1 和 lock2,用于控制线程对资源的访问。
  2. thread1 函数:

它通过一个无休止的 while 循环来不断尝试获取 lock1 锁。

一旦成功获取,会输出相应的成功获取信息,并立即跳出当前的循环。

倘若获取失败,会输出失败提示,并等待 1 秒钟,然后继续下一轮的获取尝试。

在成功获取 lock1 之后,经过 1 秒钟的模拟操作,再次进入另一个 while 循环,尝试获取 lock2 。

若获取 lock2 失败,会输出失败信息,同时释放之前已获取到的 lock1 ,接着等待 1 秒钟,再次进行获取尝试。

当成功获取到 lock2 后,按顺序释放 lock2 和 lock1 ,完成线程的操作。

  1. thread2 函数:

其逻辑与 thread1 相似。首先通过无限的 while 循环尝试获取 lock2 。

成功获取时输出信息并跳出循环,失败则输出提示、等待 1 秒后重新尝试。

在成功获取 lock2 并经过 1 秒模拟操作后,进入新的循环尝试获取 lock1 。

若获取 lock1 失败,同样输出失败提示,释放 lock2 ,等待 1 秒后重试。

最终成功获取并按序释放 lock1 和 lock2 。

  1. main 函数:

声明了两个线程变量 t1 和 t2 。

对两个互斥锁进行初始化操作。

分别创建两个线程,让它们执行 thread1 和 thread2 函数。

使用 pthread_join 函数等待这两个线程完成执行。

最后,销毁两个互斥锁,释放相关资源。

这种通过循环尝试获取锁的方式,能够一定程度上应对获取锁失败的情况,避免线程因单次获取失败而直接终止或出现错误,增强了程序的稳定性和适应性。但需要注意,这种频繁的重试可能会带来一定的系统开销,所以在实际运用中,要根据具体场景合理设定等待时间和重试策略,以达到性能和可靠性的平衡。

程序运行结果如下:

[root@localhost multi_pthread_file]# ./dead_lock_avoid
Thread 1: Acquired lock 1
Thread 2: Acquired lock 2
Thread 1: Failed to acquire lock 2, retrying...
Thread 2: Failed to acquire lock 1, retrying...
Thread 1: Acquired lock 2
Thread 2: Acquired lock 1

通过程序运行结果可知,多个线程使用 pthread_mutex_trylock 加锁,可以避免死锁。

七、总结

互斥锁(Mutex,Mutual Exclusion Lock)是一种用于多线程环境中的同步机制,具有以下关键特点和用途:

  1. 资源保护:确保在同一时刻只有一个线程能够访问被保护的共享资源,防止数据竞争和不一致性。
  2. 原子操作:保证对共享资源的操作是原子性的,即要么完全执行,要么完全不执行,避免中间状态被其他线程观察到。
  3. 同步协调:使多个线程能够按照预定的顺序和条件进行协作,避免混乱和错误。
  4. 互斥性:当一个线程获取互斥锁后,其他线程在该锁被释放之前无法获取,从而实现对关键代码段或资源的独占访问。

在使用互斥锁时,需要注意正确的加锁和解锁顺序,避免死锁等问题。同时,过度使用互斥锁可能导致性能下降,因为线程可能会因等待锁而阻塞。因此,在设计多线程程序时,需要仔细考虑互斥锁的使用位置和时机,以达到最佳的性能和正确性平衡。

责任编辑:武晓燕 来源: Linux二进制
相关推荐

2024-07-05 08:32:36

2024-07-08 12:51:05

2024-07-25 11:53:53

2010-01-21 11:27:30

linux多线程机制线程同步

2020-09-28 06:49:50

Linux系统编程互斥量mutex

2010-03-15 16:31:34

Java多线程

2020-08-26 08:59:58

Linux线程互斥锁

2011-11-23 10:09:19

Java线程机制

2021-03-24 08:02:58

C语言

2024-06-24 08:10:00

C++互斥锁

2023-12-24 12:33:20

互斥锁Go代码

2023-06-02 08:29:24

https://wwMutex

2023-06-09 07:59:37

多线程编程锁机制

2009-11-28 20:24:13

Linux互斥锁同步移植

2017-12-15 10:20:56

MySQLInnoDB同步机制

2019-05-27 14:40:43

Java同步机制多线程编程

2012-07-27 10:02:39

C#

2012-07-09 09:25:13

ibmdw

2024-10-14 08:51:52

协程Go语言

2016-09-20 15:21:35

LinuxInnoDBMysql
点赞
收藏

51CTO技术栈公众号