多线程----互斥

以下是关于多线程中互斥相关的详细知识点介绍以及C语言代码示例:

一、互斥的概念与重要性

在多线程编程中,多个线程可能会同时访问和操作共享资源(如全局变量、共享的数据结构等)。如果没有适当的控制机制,就可能导致数据不一致、程序逻辑错误等问题,这就是所谓的"线程安全"问题。互斥(Mutual Exclusion)就是一种用于解决这类问题的机制,它确保在任何时刻,只有一个线程能够访问被保护的共享资源,从而避免多个线程同时对共享资源进行读写操作造成的冲突。

例如,假设有两个线程同时对一个全局的计数器变量进行自增操作。如果没有互斥机制,可能会出现以下情况:线程A读取计数器的值,还没来得及更新写入,线程B也读取了相同的值,然后两个线程分别对读取到的值进行自增并写回,这样最终计数器的值就会比预期的少增加了一次,导致数据不一致。

二、C语言中实现互斥的方式(使用 pthread 库)

1. pthread_mutex_t 类型与互斥锁初始化
  • pthread_mutex_t 类型 :这是C语言中用于表示互斥锁的类型,它在头文件 <pthread.h> 中定义。互斥锁就是实现互斥机制的核心元素,通过操作这个互斥锁来控制对共享资源的访问权限。
  • pthread_mutex_init 函数:用于初始化互斥锁,其函数原型如下:
c 复制代码
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
  • 参数说明
    • mutex:是一个指向 pthread_mutex_t 类型的指针,指向要初始化的互斥锁变量。
    • attr:是一个指向 pthread_mutexattr_t 类型的指针,用于设置互斥锁的属性。如果设置为 NULL,则使用默认属性。
  • 返回值 :成功时返回0,失败时返回错误码,通过 perror 等函数可以输出错误信息。

以下是一个初始化互斥锁的简单示例代码:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_mutex_t mutex;

int main() {
    int result = pthread_mutex_init(&mutex, NULL);
    if (result!= 0) {
        perror("互斥锁初始化失败");
        return -1;
    }
    // 这里可以继续后续使用互斥锁的代码逻辑
    // 例如创建线程等操作,后续再进行讲解
    result = pthread_mutex_destroy(&mutex);
    if (result!= 0) {
        perror("互斥锁销毁失败");
        return -1;
    }
    return 0;
}
2. 互斥锁的获取(pthread_mutex_lock 函数)
  • 函数原型
c 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 作用与原理 :当一个线程调用 pthread_mutex_lock 函数尝试获取互斥锁时,它会首先检查互斥锁的状态。如果互斥锁当前处于未锁定状态,那么该线程会成功获取互斥锁,即将互斥锁设置为锁定状态,然后继续执行后续访问共享资源的代码;如果互斥锁已经被其他线程锁定了,那么当前线程会被阻塞,进入等待状态,直到该互斥锁被释放(即其他线程调用 pthread_mutex_unlock 函数释放锁),此时当前线程才有可能获取到互斥锁并继续执行。

以下是一个简单的示例,展示两个线程竞争获取互斥锁来访问共享资源(这里以一个简单的全局变量模拟共享资源):

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex;
int shared_variable = 0;

void *thread_function(void *arg) {
    pthread_mutex_lock(&mutex);
    shared_variable++;
    printf("线程 %ld 访问了共享变量,当前值为:%d\n", pthread_self(), shared_variable);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t thread1, thread2;
    int result;

    result = pthread_mutex_init(&mutex, NULL);
    if (result!= 0) {
        perror("互斥锁初始化失败");
        return -1;
    }

    result = pthread_create(&thread1, NULL, thread_function, NULL);
    if (result!= 0) {
        perror("线程1创建失败");
        return -1;
    }

    result = pthread_create(&thread2, NULL, thread_function, NULL);
    if (result!= 0) {
        perror("线程2创建失败");
        return -1;
    }

    result = pthread_join(thread1, NULL);
    if (result!= 0) {
        perror("等待线程1结束失败");
        return -1;
    }

    result = pthread_join(thread2, NULL);
    if (result!= 0) {
        perror("等待线程2结束失败");
        return -1;
    }

    result = pthread_mutex_destroy(&mutex);
    if (result!= 0) {
        perror("互斥锁销毁失败");
        return -1;
    }

    return 0;
}

在这个示例中:

  • 首先定义了一个互斥锁 mutex 和一个全局的共享变量 shared_variable
  • thread_function 函数中,每个线程在访问共享变量 shared_variable 之前,都先调用 pthread_mutex_lock 函数来获取互斥锁,确保同一时刻只有一个线程能对共享变量进行操作(自增操作并打印其值),操作完成后调用 pthread_mutex_unlock 函数释放互斥锁,以便其他线程可以获取锁来访问共享资源。
3. 互斥锁的释放(pthread_mutex_unlock 函数)
  • 函数原型
c 复制代码
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 作用与原理 :当持有互斥锁的线程完成对共享资源的访问后,需要调用 pthread_mutex_unlock 函数来释放互斥锁,将互斥锁的状态从锁定变回未锁定状态。这样,其他被阻塞等待该互斥锁的线程就有机会获取到锁,进而访问共享资源。如果没有正确地释放互斥锁(比如线程提前结束或者出现异常没有执行到释放锁的代码),可能会导致其他线程一直处于阻塞状态,出现死锁等问题。

继续以刚才的示例来说明,在 thread_function 函数中,线程对共享变量操作完毕后,通过 pthread_mutex_unlock 函数及时释放了互斥锁,保证了整个多线程访问共享资源的流程能够正常循环进行下去,其他线程可以轮流获取锁来访问共享资源。

4. 互斥锁的销毁(pthread_mutex_destroy 函数)
  • 函数原型
c 复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 作用与原理 :当不再需要使用某个互斥锁时(比如相关的线程都已经结束,不再有对共享资源的并发访问需求了),可以调用 pthread_mutex_destroy 函数来销毁互斥锁,释放其占用的系统资源。需要注意的是,在销毁互斥锁之前,必须确保该互斥锁当前处于未锁定状态,否则销毁操作会失败。

在前面的示例代码中,无论是在正常执行完毕所有线程操作后(在 main 函数结尾处),还是在出现错误提前返回之前(对应的错误处理分支中),都对互斥锁进行了正确的销毁操作,以保证资源的合理释放和程序的稳定性。

三、互斥在实际应用中的一些注意事项

1. 合理的临界区设置
  • 临界区是指访问共享资源的代码段,在这个区域内需要通过互斥锁来进行保护。要确保临界区的范围尽量小,只包含对共享资源进行实际读写操作的必要代码,避免将过多无关的代码也纳入临界区,这样可以减少线程阻塞等待锁的时间,提高程序的并发性能。例如,如果在一个函数中有一部分代码是操作共享变量的,另一部分代码只是普通的局部变量运算等与共享资源无关的操作,那么就只应该将操作共享变量的那部分代码放在获取和释放互斥锁之间作为临界区。
2. 避免死锁情况
  • 死锁是多线程编程中一种严重的问题,指的是多个线程相互等待对方释放资源(比如互斥锁),从而导致所有线程都无法继续执行的情况。常见的死锁产生原因有循环等待、资源独占且不释放等。例如,线程A获取了互斥锁1,然后等待获取互斥锁2;而线程B获取了互斥锁2,又等待获取互斥锁1,这样两个线程就陷入了互相等待的死锁状态。为避免死锁,可以采用一些策略,如按照固定的顺序获取互斥锁、使用超时机制等来进行预防和处理。
3. 性能考虑
  • 虽然互斥锁能保证共享资源的安全访问,但过多使用互斥锁或者不合理的锁获取释放频率都会影响程序的性能。因为获取和释放锁本身是有一定开销的,而且线程阻塞等待锁的过程也会占用系统资源。所以在设计多线程程序时,要权衡好数据安全和性能之间的关系,根据实际情况选择合适的共享资源划分和互斥锁使用方式。

总之,互斥是多线程编程中保障共享资源安全访问的重要机制,在C语言中通过 pthread 库提供的相关函数可以有效地实现互斥操作,但在实际应用中需要充分考虑各种注意事项,以确保程序的正确性和高效性。

互斥的加锁解锁操作

  1. 互斥量加锁操作

    • 概念与目的

      • 互斥量加锁操作的主要目的是获取对共享资源的独占访问权。当一个线程对互斥量进行加锁时,它实际上是在向系统表明自己想要独占访问受该互斥量保护的共享资源。如果此时互斥量没有被其他线程锁住,那么这个线程就能成功获取锁,从而可以安全地访问共享资源。
    • 实现原理(以操作系统层面为例)

      • 在操作系统底层,互斥量通常是通过原子操作和内核提供的同步机制来实现加锁。原子操作确保了在多线程环境下,互斥量状态的检查和设置(从"未锁定"到"锁定")是一个不可分割的操作,不会被其他线程中断。例如,许多处理器架构都支持比较并交换(CAS - Compare and Swap)指令,这个指令可以原子地比较互斥量的当前状态和期望状态,并在两者匹配时进行交换操作。
      • 当一个线程尝试加锁时,系统首先会检查互斥量的状态位。如果状态位显示互斥量未被锁定,系统会使用原子操作将状态位设置为锁定,并将该线程标记为互斥量的拥有者。如果状态位显示互斥量已经被锁定,那么这个线程就会进入等待队列,这个过程可能涉及到操作系统的线程调度,线程会被阻塞,暂停执行,直到互斥量被解锁。
    • 代码示例(以C++的 std::mutex 为例)

      cpp 复制代码
      #include <iostream>
      #include <thread>
      #include <mutex>
      
      std::mutex mtx;
      int shared_variable = 0;
      
      void increment() {
          mtx.lock();
          // 进入临界区,开始访问共享资源
          shared_variable++;
          std::cout << "当前共享变量的值为: " << shared_variable << std::endl;
          // 离开临界区,释放互斥量
          mtx.unlock();
      }
      
      int main() {
          std::thread t1(increment);
          std::thread t2(increment);
          t1.join();
          t2.join();
          return 0;
      }
      • 在这个示例中,mtx.lock() 就是互斥量的加锁操作。在 increment 函数中,当线程执行到这一步时,会尝试获取互斥量 mtx 的锁。如果成功获取,就可以进入下面的代码块,对共享变量 shared_variable 进行自增操作并输出其值。这个代码块被称为"临界区",在临界区内对共享资源的访问是受互斥量保护的。
  2. 互斥量解锁操作

    • 概念与目的
      • 互斥量解锁操作是与加锁操作相对应的,其目的是释放对共享资源的独占访问权,使得其他等待该互斥量的线程有机会获取锁并访问共享资源。当一个线程完成对共享资源的访问后,必须及时解锁互斥量,否则可能会导致其他线程一直处于等待状态,造成程序死锁或者资源浪费。
    • 实现原理
      • 在操作系统层面,解锁操作同样需要原子性来保证其正确性。当一个线程调用互斥量的解锁操作时,系统会首先检查调用解锁操作的线程是否是互斥量的当前拥有者。如果是,就将互斥量的状态位从"锁定"设置为"未锁定",然后检查等待队列中是否有等待的线程。如果有等待线程,系统会根据一定的调度策略(如先进先出)唤醒其中一个线程,使其能够获取互斥量并继续执行。
    • 代码示例(继续以上面C++示例为例)
      • increment 函数中,mtx.unlock() 就是互斥量的解锁操作。当线程完成对共享变量 shared_variable 的操作后,通过这个操作释放互斥量,使得其他线程(在这个例子中是另一个 increment 函数所在的线程)能够获取锁,从而保证多个线程可以按照顺序安全地访问共享资源。如果忘记了在合适的位置进行解锁操作,可能会导致程序出现异常行为,比如只有一个线程能够一直访问共享资源,其他线程被永久阻塞。
  3. 加锁与解锁操作的注意事项

    • 配对使用
      • 加锁和解锁操作必须配对使用。对于每一个成功的加锁操作,都应该有一个对应的解锁操作,而且解锁操作应该在对共享资源的访问完成之后及时进行。如果在一个函数或者代码块中加锁,但是在异常情况(如抛出异常)或者错误的代码路径下没有解锁,就可能会导致死锁。为了避免这种情况,一些编程语言提供了RAII(Resource Acquisition Is Initialization)机制来自动管理互斥量的生命周期,如C++中的 std::lock_guard
    • 临界区大小控制
      • 加锁和解锁操作之间的代码区域(临界区)应该尽量小。只应该包含对共享资源进行实际访问(读或写)的代码,避免将不必要的长时间运行的代码或者不涉及共享资源的代码包含在临界区内。因为在临界区内,其他线程是无法获取互斥量并访问共享资源的,临界区过长会降低程序的并发性能。例如,如果在临界区内有一个耗时很长的循环操作,而这个循环操作只有部分代码涉及共享资源,那么应该将涉及共享资源的部分提取出来,缩小临界区的范围。

互斥量加锁与解锁的原则

  1. 配对原则

    • 必要性:加锁和解锁操作必须成对出现。每一次成功的加锁操作都应该对应一次解锁操作。这就好比你拿到了一把钥匙(加锁)打开了一个房间(获取共享资源的访问权限),在离开房间的时候(完成共享资源访问)必须归还钥匙(解锁),这样其他人才有机会进入房间。
    • 异常处理情况 :在实际编程中,即使出现异常情况,也要保证解锁操作能够执行。例如,在C++ 中,可以使用try - catch块来捕获异常,在catch块中执行互斥量的解锁操作。或者使用RAII(Resource Acquisition Is Initialization)机制,如std::lock_guardstd::unique_lock,这些类在构造函数中进行加锁,在析构函数中自动进行解锁,即使在函数执行过程中出现异常导致提前返回,也能保证互斥量被正确解锁。
    • 示例(C++)
      • 不使用RAII机制,手动处理异常和解锁:

        cpp 复制代码
        #include <iostream>
        #include <thread>
        #include <mutex>
        
        std::mutex mtx;
        void func() {
            mtx.lock();
            try {
                // 访问共享资源,可能会抛出异常
                throw std::runtime_error("发生错误");
            } catch (const std::runtime_error& e) {
                std::cerr << e.what() << std::endl;
                mtx.unlock();
                return;
            }
            mtx.unlock();
        }
        
        int main() {
            std::thread t(func);
            t.join();
            return 0;
        }
      • 使用std::lock_guard实现自动解锁:

        cpp 复制代码
        #include <iostream>
        #include <thread>
        #include <mutex>
        
        std::mutex mtx;
        void func() {
            std::lock_guard<std::mutex> guard(mtx);
            // 访问共享资源,即使抛出异常也会自动解锁
            throw std::runtime_error("发生错误");
        }
        
        int main() {
            std::thread t(func);
            t.join();
            return 0;
        }
  2. 最小化临界区原则

    • 临界区定义和影响 :临界区是指在加锁和解锁操作之间的代码部分,这段代码是对共享资源进行访问的部分。如果临界区过大,会导致其他线程长时间等待获取互斥量,从而降低程序的并发性能。例如,在一个多线程的服务器程序中,如果对整个请求处理过程(包括读取请求、处理请求、发送响应)都进行加锁,那么在处理一个请求时,其他请求都无法同时处理,大大降低了服务器的处理效率。

    • 确定最小临界区的方法:仔细分析代码,只将真正需要访问共享资源的代码放在临界区内。如果有一个函数包含了对共享资源的访问和其他非共享资源相关的操作,应该将对共享资源的访问部分提取出来,单独放在临界区内。例如,有一个函数既更新了全局共享变量,又进行了一些本地变量的计算和输出,那么应该只对更新全局共享变量的部分进行加锁。

    • 示例(C)

      • 假设存在一个全局变量count和一个本地变量local_var,以下是不好的做法:

        c 复制代码
        #include <stdio.h>
        #include <pthread.h>
        
        pthread_mutex_t mutex;
        int count = 0;
        
        void *thread_func(void *arg) {
            pthread_mutex_lock(&mutex);
            int local_var = 0;
            // 进行一些本地变量的操作,不涉及共享资源,不应该放在临界区内
            for (int i = 0; i < 1000; ++i) {
                local_var++;
            }
            count++;  // 访问共享资源,这部分应该在临界区内
            printf("Count: %d, Local Var: %d\n", count, local_var);
            pthread_mutex_unlock(&mutex);
            return NULL;
        }
        
        int main() {
            pthread_t threads[2];
            pthread_mutex_init(&mutex, NULL);
            for (int i = 0; i < 2; ++i) {
                pthread_create(&threads[i], NULL, thread_func, NULL);
            }
            for (int i = 0; i < 2; ++i) {
                pthread_join(threads[i], NULL);
            }
            pthread_mutex_destroy(&mutex);
            return 0;
        }
      • 改进后的做法:

        c 复制代码
        #include <stdio.h>
        #include <pthread.h>
        
        pthread_mutex_t mutex;
        int count = 0;
        
        void *thread_func(void *arg) {
            int local_var = 0;
            // 进行本地变量的操作,不涉及共享资源
            for (int i = 0; i < 1000; ++i) {
                local_var++;
            }
            pthread_mutex_lock(&mutex);
            count++;  // 访问共享资源,这部分放在临界区内
            pthread_mutex_unlock(&mutex);
            printf("Count: %d, Local Var: %d\n", count, local_var);
            return NULL;
        }
        
        int main() {
            pthread_t threads[2];
            pthread_mutex_init(&mutex, NULL);
            for (int i = 0; i < 2; ++i) {
                pthread_create(&threads[i], NULL, thread_func, NULL);
            }
            for (int i = 0; i < 2; ++i) {
                pthread_join(threads[i], NULL);
            }
            pthread_mutex_destroy(&mutex);
            return 0;
        }
  3. 同一互斥量原则

    • 原则内容 :在一个代码区域内,对于一组相关的共享资源访问操作,应该使用同一个互斥量进行加锁和解锁。如果使用多个不同的互斥量来保护相关的共享资源,可能会导致死锁或者数据不一致的情况。例如,假设有两个互斥量mutex1mutex2,线程A先获取了mutex1,然后试图获取mutex2;同时线程B先获取了mutex2,然后试图获取mutex1,这样就会导致两个线程互相等待,形成死锁。

    • 避免死锁的策略 :一种常见的策略是按照相同的顺序获取互斥量。例如,在整个程序中,所有需要获取多个互斥量的地方,都按照先获取mutex1,再获取mutex2等固定的顺序进行操作,这样可以避免循环等待导致的死锁。

    • 示例(C++) - 可能导致死锁的情况

      cpp 复制代码
      #include <iostream>
      #include <thread>
      #include <mutex>
      
      std::mutex mutex1;
      std::mutex mutex2;
      
      void thread1_func() {
          mutex1.lock();
          std::cout << "线程1获取了互斥量1" << std::endl;
          // 模拟一些操作
          std::this_thread::sleep_for(std::chrono::milliseconds(100));
          mutex2.lock();
          std::cout << "线程1获取了互斥量2" << std::endl;
          // 访问共享资源
          mutex2.unlock();
          mutex1.unlock();
      }
      
      void thread2_func() {
          mutex2.lock();
          std::cout << "线程2获取了互斥量2" << std::endl;
          // 模拟一些操作
          std::this_thread::sleep_for(std::chrono::milliseconds(100));
          mutex1.lock();
          std::cout << "线程2获取了互斥量1" << std::endl;
          // 访问共享资源
          mutex1.unlock();
          mutex2.unlock();
      }
      
      int main() {
          std::thread t1(thread1_func);
          std::thread t2(thread2_func);
          t1.join();
          t2.join();
          return 0;
      }
    • 在这个示例中,线程1和线程2获取互斥量的顺序不同,很可能会导致死锁。可以通过按照相同的顺序获取互斥量来避免这种情况,比如在所有需要获取mutex1mutex2的地方,都先获取mutex1,再获取mutex2

如何避免多线程中的死锁问题?

  1. 按顺序获取锁

    • 原理:如果多个线程需要获取多个互斥量,确保所有线程都按照相同的顺序获取这些互斥量。这样可以避免循环等待的情况发生。例如,有互斥量A和B,所有线程都应该先获取A,再获取B,而不是有的线程先获取A再获取B,有的线程先获取B再获取A。

    • 代码示例(C++)

      cpp 复制代码
      #include <iostream>
      #include <thread>
      #include <mutex>
      
      std::mutex mutex1;
      std::mutex mutex2;
      
      void thread1_func() {
          std::lock_guard<std::mutex> guard1(mutex1);
          std::this_thread::sleep_for(std::chrono::milliseconds(100));
          std::lock_guard<std::mutex> guard2(mutex2);
          // 访问共享资源
      }
      
      void thread2_func() {
          std::lock_guard<std::mutex> guard1(mutex1);
          std::this_thread::sleep_for(std::chrono::milliseconds(100));
          std::lock_guard<std::mutex> guard2(mutex2);
          // 访问共享资源
      }
      
      int main() {
          std::thread t1(thread1_func);
          std::thread t2(thread2_func);
          t1.join();
          t2.join();
          return 0;
      }
      • 在这个示例中,两个线程都按照先获取mutex1,再获取mutex2的顺序来获取互斥量,从而避免了死锁。
  2. 避免嵌套锁

    • 原理:尽量减少在一个已经获取了锁的代码块中再去获取另一个锁的情况。如果必须要获取多个锁,考虑将获取多个锁的操作放在一个函数中,并在函数开头一次性获取所有需要的锁,而不是在嵌套的代码块中逐步获取。因为嵌套锁会增加死锁的风险,很容易出现一个线程在等待另一个线程释放它已经持有的锁的情况。

    • 代码示例(C) - 不好的做法(可能导致死锁)

      c 复制代码
      #include <stdio.h>
      #include <pthread.h>
      
      pthread_mutex_t mutex1;
      pthread_mutex_t mutex2;
      
      void *thread1(void *arg) {
          pthread_mutex_lock(&mutex1);
          // 一些操作
          pthread_mutex_lock(&mutex2);
          // 访问共享资源
          pthread_mutex_unlock(&mutex2);
          pthread_mutex_unlock(&mutex1);
          return NULL;
      }
      
      void *thread2(void *arg) {
          pthread_mutex_lock(&mutex2);
          // 一些操作
          pthread_mutex_lock(&mutex1);
          // 访问共享资源
          pthread_mutex_unlock(&mutex1);
          pthread_mutex_unlock(&mutex2);
          return NULL;
      }
      
      int main() {
          pthread_t t1, t2;
          pthread_mutex_init(&mutex1, NULL);
          pthread_mutex_init(&mutex2, NULL);
          pthread_create(&t1, NULL, thread1, NULL);
          pthread_create(&t2, NULL, thread2, NULL);
          pthread_join(t1, NULL);
          pthread_join(t2, NULL);
          pthread_mutex_destroy(&mutex1);
          pthread_mutex_destroy(&mutex2);
          return 0;
      }
      • 在这个示例中,thread1thread2分别在获取一个锁之后又尝试获取另一个锁,而且获取的顺序不同,这很容易导致死锁。
    • 改进后的代码(降低死锁风险)

      c 复制代码
      #include <stdio.h>
      #include <pthread.h>
      
      pthread_mutex_t mutex1;
      pthread_mutex_t mutex2;
      
      void acquire_locks(pthread_mutex_t *m1, pthread_mutex_t *m2) {
          pthread_mutex_lock(m1);
          pthread_mutex_lock(m2);
      }
      
      void release_locks(pthread_mutex_t *m1, pthread_mutex_t *m2) {
          pthread_mutex_unlock(m2);
          pthread_mutex_unlock(m1);
      }
      
      void *thread1(void *arg) {
          acquire_locks(&mutex1, &mutex2);
          // 访问共享资源
          release_locks(&mutex1, &mutex2);
          return NULL;
      }
      
      void *thread2(void *arg) {
          acquire_locks(&mutex1, &mutex2);
          // 访问共享资源
          release_locks(&mutex1, &mutex2);
          return NULL;
      }
      
      int main() {
          pthread_t t1, t2;
          pthread_mutex_init(&mutex1, NULL);
          pthread_mutex_init(&mutex2, NULL);
          pthread_create(&t1, NULL, thread1, NULL);
          pthread_create(&t2, NULL, thread2, NULL);
          pthread_join(t1, NULL);
          pthread_join(t2, NULL);
          pthread_mutex_destroy(&mutex1);
          pthread_mutex_destroy(&mutex2);
          return 0;
      }
      • 在改进后的代码中,通过acquire_locks函数来一次性获取两个锁,所有线程都按照相同的顺序获取锁,降低了死锁的风险。
  3. 使用超时机制获取锁

    • 原理:在尝试获取锁时,设置一个超时时间。如果在超时时间内无法获取锁,就放弃获取,执行其他操作或者返回错误。这样可以避免线程一直等待某个无法获取的锁而导致死锁。不过这种方法需要操作系统或者编程语言提供相应的支持来实现超时获取锁的功能。

    • 代码示例(以Python的threading模块为例,模拟超时机制)

      python 复制代码
      import threading
      import time
      
      mutex1 = threading.Lock()
      mutex2 = threading.Lock()
      
      def thread1():
          start_time = time.time()
          while True:
              acquired_m1 = mutex1.acquire(timeout=1)
              if acquired_m1:
                  try:
                      acquired_m2 = mutex2.acquire(timeout=1)
                      if acquired_m2:
                          try:
                              # 访问共享资源
                              break
                          finally:
                              mutex2.release()
                      else:
                          print("线程1无法获取mutex2,放弃")
                          mutex1.release()
                  except threading.TimeoutError:
                      print("线程1获取mutex2超时,放弃")
                      mutex1.release()
              else:
                  print("线程1获取mutex1超时,放弃")
                  break
              if time.time() - start_time > 5:
                  print("线程1等待时间过长,放弃")
                  break
      
      def thread2():
          start_time = time.time()
          while True:
              acquired_m2 = mutex2.acquire(timeout=1)
              if acquired_m2:
                  try:
                      acquired_m1 = mutex1.acquire(timeout=1)
                      if acquired_m1:
                          try:
                              # 访问共享资源
                              break
                          finally:
                              mutex1.release()
                      else:
                          print("线程2无法获取mutex1,放弃")
                          mutex2.release()
                  except threading.TimeoutError:
                      print("线程2获取mutex1超时,放弃")
                      mutex2.release()
              else:
                  print("线程2获取mutex2超时,放弃")
                  break
              if time.time() - start_time > 5:
                  print("线程2等待时间过长,放弃")
                  break
      
      t1 = threading.Thread(target=thread1)
      t2 = threading.Thread(target=thread2)
      t1.start()
      t2.start()
      t1.join()
      t2.join()
      • 在这个Python示例中,虽然Python的Lock类本身没有原生的超时获取功能,但通过在一个循环中不断尝试获取锁,并设置每次尝试的超时时间,模拟了超时机制。如果在规定时间内无法获取锁,线程就会放弃获取,从而避免了一直等待导致死锁的情况。不过这种方法比较复杂,并且可能会因为频繁地尝试获取锁而消耗一定的资源。
  4. 资源层次化

    • 原理 :将共享资源划分成不同的层次,规定线程只能按照从低层次资源到高层次资源的顺序获取资源。例如,将文件资源分为低层次资源,数据库连接资源分为高层次资源。当线程已经获取了高层次资源时,就不能再去获取低层次资源,这样可以避免循环等待资源的情况。

    • 示例场景:假设有一个文件读写操作和一个数据库插入操作,它们分别需要获取对应的资源锁。可以将文件资源看作是低层次资源,数据库资源看作是高层次资源。如果一个线程已经获取了数据库资源锁,就不允许它再去获取文件资源锁,除非它先释放数据库资源锁。通过这种方式来组织资源的获取顺序,减少死锁的可能性。

相关推荐
zhojiew1 分钟前
istio in action之流量控制与路由
java·数据库·istio
D_aniel_16 分钟前
排序算法-归并排序
java·排序算法·归并排序
可儿·四系桜1 小时前
WebSocket:实时通信的新时代
java·网络·websocket·网络协议
forestsea1 小时前
Maven 插件机制与生命周期管理
java·maven
七月在野,八月在宇,九月在户1 小时前
maven 依赖冲突异常分析
java·maven
金融数据出海1 小时前
黄金、碳排放期货市场API接口文档
java·开发语言·spring boot·后端·金融·区块链
胡斌附体1 小时前
微服务中 本地启动 springboot 无法找到nacos配置 启动报错
java·spring boot·微服务·yml·naocs yml
薯条不要番茄酱2 小时前
【JVM】从零开始深度解析JVM
java·jvm
夏季疯2 小时前
学习笔记:黑马程序员JavaWeb开发教程(2025.3.31)
java·笔记·学习
chunfeng—2 小时前
Redis相关命令详解与原理(一)
数据库·redis·缓存