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

原创 Linux二进制 2024-06-28 08:18

引言

在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 相关的函数原型:

  1. 初始化互斥锁

    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 结构的指针。调用此函数将释放互斥锁,允许其他线程锁定它。

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

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

      pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

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

    2. 动态初始化: 使用 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. 死锁的条件:死锁通常发生在以下四个条件同时满足时:

    • 互斥:资源不能被多个线程同时访问。
    • 占有和等待:一个线程持有至少一个锁,并且等待获取其他线程持有的锁。
    • 不可剥夺:已经获得的锁不能被其他线程强行剥夺,只能由持有它的线程释放。
    • 循环等待:存在一个线程持有锁的循环链,每个线程都在等待下一个线程持有的锁。
  • 死锁的避免:可以通过以下几种策略来避免死锁:

    • 固定顺序加锁:总是以相同的顺序获取多个锁。
    • 超时尝试加锁:使用 pthread_mutex_trylock() 或其他带有超时功能的加锁方法。
    • 一次性获取所有锁:在开始访问共享资源前,先获取所有需要的锁。
    • 使用死锁检测算法:定期检测死锁情况,并采取措施解决。
  • 死锁的解除:如果检测到死锁,可以采取以下措施:

    • 剥夺资源:从其他线程剥夺资源,赋予死锁线程。
    • 撤销进程:终止一个或多个线程或进程,打破死锁状态。
    • 资源重分配:重新分配资源,以打破循环等待条件。
  • 避免嵌套锁:尽量减少锁的嵌套使用,如果必须嵌套使用,确保内层锁总是不同类型的或者使用不同的加锁顺序。

  • 锁的分级管理:将锁分级,高级别的锁可以包含多个低级别的锁,确保在请求高级别锁时,已经持有所有需要的低级别锁。

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

    六、互斥锁实战

    1、互斥锁加解锁

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

    #include 
    #include
    #include

    #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 
    #include
    #include

    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 命令找到进程 IDPID),然后使用 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 
    #include
    #include
    #include
    #include

    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,完成操作后释放两个锁并重置标志。
  • thread2 函数:

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

    // 互斥锁
    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,最后释放。
  • detectDeadlock 函数用于模拟死锁检测,但由于当前固定了加锁顺序,实际上不会发生死锁,所以此函数内未进行真正的检测操作。
  • 在 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 
    #include
    #include

    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 ,完成线程的操作。
  • thread2 函数:

    • 其逻辑与 thread1 相似。首先通过无限的 while 循环尝试获取 lock2 。
    • 成功获取时输出信息并跳出循环,失败则输出提示、等待 1 秒后重新尝试。
    • 在成功获取 lock2 并经过 1 秒模拟操作后,进入新的循环尝试获取 lock1 。
    • 若获取 lock1 失败,同样输出失败提示,释放 lock2 ,等待 1 秒后重试。
    • 最终成功获取并按序释放 lock1 和 lock2 。
  • 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二进制 Linux编程、内核模块、网络原创文章分享,欢迎关注"Linux二进制"微信公众号
    评论
    •  万万没想到!科幻电影中的人形机器人,正在一步步走进我们人类的日常生活中来了。1月17日,乐聚将第100台全尺寸人形机器人交付北汽越野车,再次吹响了人形机器人疯狂进厂打工的号角。无独有尔,银河通用机器人作为一家成立不到两年时间的创业公司,在短短一年多时间内推出革命性的第一代产品Galbot G1,这是一款轮式、双臂、身体可折叠的人形机器人,得到了美团战投、经纬创投、IDG资本等众多投资方的认可。作为一家成立仅仅只有两年多时间的企业,智元机器人也把机器人从梦想带进了现实。2024年8月1
      刘旷 2025-01-21 11:15 399浏览
    • 高速先生成员--黄刚这不马上就要过年了嘛,高速先生就不打算给大家上难度了,整一篇简单但很实用的文章给大伙瞧瞧好了。相信这个标题一出来,尤其对于PCB设计工程师来说,心就立马凉了半截。他们辛辛苦苦进行PCB的过孔设计,高速先生居然说设计多大的过孔他们不关心!另外估计这时候就跳出很多“挑刺”的粉丝了哈,因为翻看很多以往的文章,高速先生都表达了过孔孔径对高速性能的影响是很大的哦!咋滴,今天居然说孔径不关心了?别,别急哈,听高速先生在这篇文章中娓娓道来。首先还是要对各位设计工程师的设计表示肯定,毕竟像我
      一博科技 2025-01-21 16:17 101浏览
    •     IPC-2581是基于ODB++标准、结合PCB行业特点而指定的PCB加工文件规范。    IPC-2581旨在替代CAM350格式,成为PCB加工行业的新的工业规范。    有一些免费软件,可以查看(不可修改)IPC-2581数据文件。这些软件典型用途是工艺校核。    1. Vu2581        出品:Downstream     
      电子知识打边炉 2025-01-22 11:12 55浏览
    •  光伏及击穿,都可视之为 复合的逆过程,但是,复合、光伏与击穿,不单是进程的方向相反,偏置状态也不一样,复合的工况,是正偏,光伏是零偏,击穿与漂移则是反偏,光伏的能源是外来的,而击穿消耗的是结区自身和电源的能量,漂移的载流子是 客席载流子,须借外延层才能引入,客席载流子 不受反偏PN结的空乏区阻碍,能漂不能漂,只取决于反偏PN结是否处于外延层的「射程」范围,而穿通的成因,则是因耗尽层的过度扩张,致使跟 端子、外延层或其他空乏区 碰触,当耗尽层融通,耐压 (反向阻断能力) 即告彻底丧失,
      MrCU204 2025-01-17 11:30 182浏览
    • Ubuntu20.04默认情况下为root账号自动登录,本文介绍如何取消root账号自动登录,改为通过输入账号密码登录,使用触觉智能EVB3568鸿蒙开发板演示,搭载瑞芯微RK3568,四核A55处理器,主频2.0Ghz,1T算力NPU;支持OpenHarmony5.0及Linux、Android等操作系统,接口丰富,开发评估快人一步!添加新账号1、使用adduser命令来添加新用户,用户名以industio为例,系统会提示设置密码以及其他信息,您可以根据需要填写或跳过,命令如下:root@id
      Industio_触觉智能 2025-01-17 14:14 122浏览
    • 现在为止,我们已经完成了Purple Pi OH主板的串口调试和部分配件的连接,接下来,让我们趁热打铁,完成剩余配件的连接!注:配件连接前请断开主板所有供电,避免敏感电路损坏!1.1 耳机接口主板有一路OTMP 标准四节耳机座J6,具备进行音频输出及录音功能,接入耳机后声音将优先从耳机输出,如下图所示:1.21.2 相机接口MIPI CSI 接口如上图所示,支持OV5648 和OV8858 摄像头模组。接入摄像头模组后,使用系统相机软件打开相机拍照和录像,如下图所示:1.3 以太网接口主板有一路
      Industio_触觉智能 2025-01-20 11:04 150浏览
    • 嘿,咱来聊聊RISC-V MCU技术哈。 这RISC-V MCU技术呢,简单来说就是基于一个叫RISC-V的指令集架构做出的微控制器技术。RISC-V这个啊,2010年的时候,是加州大学伯克利分校的研究团队弄出来的,目的就是想搞个新的、开放的指令集架构,能跟上现代计算的需要。到了2015年,专门成立了个RISC-V基金会,让这个架构更标准,也更好地推广开了。这几年啊,这个RISC-V的生态系统发展得可快了,好多公司和机构都加入了RISC-V International,还推出了不少RISC-V
      丙丁先生 2025-01-21 12:10 112浏览
    • 2024年是很平淡的一年,能保住饭碗就是万幸了,公司业绩不好,跳槽又不敢跳,还有一个原因就是老板对我们这些员工还是很好的,碍于人情也不能在公司困难时去雪上加霜。在工作其间遇到的大问题没有,小问题还是有不少,这里就举一两个来说一下。第一个就是,先看下下面的这个封装,你能猜出它的引脚间距是多少吗?这种排线座比较常规的是0.6mm间距(即排线是0.3mm间距)的,而这个规格也是我们用得最多的,所以我们按惯性思维来看的话,就会认为这个座子就是0.6mm间距的,这样往往就不会去细看规格书了,所以这次的运气
      wuliangu 2025-01-21 00:15 186浏览
    • 临近春节,各方社交及应酬也变得多起来了,甚至一月份就排满了各式约见。有的是关系好的专业朋友的周末“恳谈会”,基本是关于2025年经济预判的话题,以及如何稳定工作等话题;但更多的预约是来自几个客户老板及副总裁们的见面,他们为今年的经济预判与企业发展焦虑而来。在聊天过程中,我发现今年的聊天有个很有意思的“点”,挺多人尤其关心我到底是怎么成长成现在的多领域风格的,还能掌握一些经济趋势的分析能力,到底学过哪些专业、在企业管过哪些具体事情?单单就这个一个月内,我就重复了数次“为什么”,再辅以我上次写的:《
      牛言喵语 2025-01-22 17:10 41浏览
    • 数字隔离芯片是一种实现电气隔离功能的集成电路,在工业自动化、汽车电子、光伏储能与电力通信等领域的电气系统中发挥着至关重要的作用。其不仅可令高、低压系统之间相互独立,提高低压系统的抗干扰能力,同时还可确保高、低压系统之间的安全交互,使系统稳定工作,并避免操作者遭受来自高压系统的电击伤害。典型数字隔离芯片的简化原理图值得一提的是,数字隔离芯片历经多年发展,其应用范围已十分广泛,凡涉及到在高、低压系统之间进行信号传输的场景中基本都需要应用到此种芯片。那么,电气工程师在进行电路设计时到底该如何评估选择一
      华普微HOPERF 2025-01-20 16:50 73浏览
    • 本文介绍瑞芯微开发板/主板Android配置APK默认开启性能模式方法,开启性能模式后,APK的CPU使用优先级会有所提高。触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。源码修改修改源码根目录下文件device/rockchip/rk3562/package_performance.xml并添加以下内容,注意"+"号为添加内容,"com.tencent.mm"为AP
      Industio_触觉智能 2025-01-17 14:09 164浏览
    • 日前,商务部等部门办公厅印发《手机、平板、智能手表(手环)购新补贴实施方案》明确,个人消费者购买手机、平板、智能手表(手环)3类数码产品(单件销售价格不超过6000元),可享受购新补贴。每人每类可补贴1件,每件补贴比例为减去生产、流通环节及移动运营商所有优惠后最终销售价格的15%,每件最高不超过500元。目前,京东已经做好了承接手机、平板等数码产品国补优惠的落地准备工作,未来随着各省市关于手机、平板等品类的国补开启,京东将第一时间率先上线,满足消费者的换新升级需求。为保障国补的真实有效发放,基于
      华尔街科技眼 2025-01-17 10:44 221浏览
    我要评论
    0
    点击右上角,分享到朋友圈 我知道啦
    请使用浏览器分享功能 我知道啦