📜  Linux 线程同步的互斥锁

📅  最后修改于: 2022-05-13 01:56:11.616000             🧑  作者: Mango

Linux 线程同步的互斥锁

先决条件: C中的多线程

线程同步被定义为一种机制,可确保两个或多个并发进程或线程不会同时执行某些称为临界区的特定程序段。进程对临界区的访问是通过使用同步技术来控制的。当一个线程开始执行临界区(程序的序列化段)时,另一个线程应该等待第一个线程完成。如果没有应用适当的同步技术,可能会导致竞争条件,其中变量的值可能无法预测,并且会根据进程或线程的上下文切换时间而变化。

线程同步问题
研究同步问题的示例代码:

#include 
#include 
#include 
#include 
#include 
  
pthread_t tid[2];
int counter;
  
void* trythis(void* arg)
{
    unsigned long i = 0;
    counter += 1;
    printf("\n Job %d has started\n", counter);
  
    for (i = 0; i < (0xFFFFFFFF); i++)
        ;
    printf("\n Job %d has finished\n", counter);
  
    return NULL;
}
  
int main(void)
{
    int i = 0;
    int error;
  
    while (i < 2) {
        error = pthread_create(&(tid[i]), NULL, &trythis, NULL);
        if (error != 0)
            printf("\nThread can't be created : [%s]", strerror(error));
        i++;
    }
  
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
  
    return 0;
}

如何编译上面的程序?
要使用 gcc 编译多线程程序,我们需要将它与 pthreads 库链接。以下是用于编译程序的命令。

gfg@ubuntu:~/$ gcc filename.c -lpthread

在这个例子中,创建了两个线程(作业),并且在这些线程的启动函数中,维护一个计数器以获取有关启动和完成作业编号的日志。



输出 :

Job 1 has started
Job 2 has started
Job 2 has finished
Job 2 has finished

问题:从最后两个日志中,可以看到日志“作业 2 已完成”重复了两次,而没有看到“作业 1 已完成”的日志。

为什么会发生?
通过仔细观察和可视化代码的执行,我们可以看到:

  • 日志“作业 2 已启动”在“作业 1 已启动”之后打印,因此可以轻松得出结论,当线程 1 正在处理时,调度程序调度了线程 2。
  • 如果我们认为上述假设为真,那么在作业 1 完成之前,' counter ' 变量的值会再次增加。
  • 因此,当作业 1 实际完成时,计数器的错误值会产生日志“作业 2 已完成”,然后是实际作业 2 的“作业 2 已完成”,反之亦然,因为它依赖于调度程序。
  • 所以我们看到问题不是重复的日志,而是“计数器”变量的错误值。
  • 实际问题是当第一个线程正在使用或即将使用它时,第二个线程使用了变量“counter”。
  • 换句话说,我们可以说在使用共享资源“计数器”时线程之间缺乏同步导致了问题,或者一句话我们可以说这个问题是由于两个线程之间的“同步问题”而发生的。

    如何解决?

    实现线程同步最流行的方法是使用Mutexes

    互斥体

    • 互斥锁是我们在使用共享资源之前设置并在使用之后释放的锁。
    • 设置锁定后,其他线程无法访问锁定的代码区域。
    • 所以我们看到,即使线程 2 被调度,而线程 1 没有完成访问共享资源并且代码被线程 1 使用互斥锁锁定,那么线程 2 甚至无法访问该代码区域。
    • 所以这保证了代码中共享资源的同步访问。

    互斥锁的工作

    1. 假设一个线程使用互斥锁锁定了一个代码区域并正在执行那段代码。
    2. 现在,如果调度程序决定进行上下文切换,则准备好执行同一区域的所有其他线程都将被解除阻塞。
    3. 所有线程中只有一个会执行,但如果该线程尝试执行已锁定的同一代码区域,则它将再次进入睡眠状态。
    4. 上下文切换将一次又一次地发生,但在锁定的互斥锁被释放之前,没有线程能够执行锁定的代码区域。
    5. 互斥锁只会被锁定它的线程释放。
    6. 因此,这确保了一旦一个线程锁定了一段代码,那么在锁定它的线程解锁之前,其他线程都不能执行同一区域。

    因此,该系统确保在处理共享资源时线程之间的同步。

    一个互斥体被初始化,然后通过调用以下两个函数来实现锁定:第一个函数初始化一个互斥体,通过第二个函数可以锁定代码中的任何关键区域。



      1. int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr) :创建一个互斥锁,由互斥锁引用,属性由 attr 指定。如果 attr 为 NULL,则使用默认互斥属性 (NONRECURSIVE)。

        返回值
        如果成功,pthread_mutex_init() 返回 0,并且互斥锁的状态变为初始化和解锁状态。
        如果不成功,pthread_mutex_init() 返回 -1。

      2. int pthread_mutex_lock(pthread_mutex_t *mutex) :锁定一个互斥锁对象,它标识了一个互斥锁。如果互斥锁已被另一个线程锁定,则该线程将等待互斥锁变为可用。锁定互斥锁的线程成为其当前所有者并保持所有者直到同一个线程解锁它。当互斥锁具有递归属性时,锁的使用可能会有所不同。当这种互斥锁被同一个线程多次锁定时,计数就会增加,并且不会发布等待线程。拥有线程必须调用 pthread_mutex_unlock() 相同的次数才能将计数减为零。

        返回值
        如果成功,pthread_mutex_lock() 返回 0。
        如果不成功,pthread_mutex_lock() 返回 -1。

    可以通过调用以下两个函数来解锁和销毁互斥锁:第一个函数释放锁,第二个函数销毁锁,使其无法在以后的任何地方使用。

      1. int pthread_mutex_unlock(pthread_mutex_t *mutex) :释放互斥对象。如果一个或多个线程正在等待锁定互斥锁,则 pthread_mutex_unlock() 会导致这些线程之一从 pthread_mutex_lock() 返回并获取互斥锁对象。如果没有线程正在等待互斥锁,则互斥锁将在没有当前所有者的情况下解锁。当互斥锁具有递归属性时,锁的使用可能会有所不同。当这种互斥锁被同一个线程多次锁定时,unlock 将递减计数并且不发布等待线程以继续使用锁运行。如果计数递减为零,则互斥锁被释放,并且如果有任何线程正在等待它被发布。

        返回值
        如果成功,pthread_mutex_unlock() 返回 0。
        如果不成功,pthread_mutex_unlock() 返回 -1

      2. int pthread_mutex_destroy(pthread_mutex_t *mutex) :删除一个互斥锁对象,它标识了一个互斥锁。互斥体用于保护共享资源。 mutex 被设置为无效值,但可以使用 pthread_mutex_init() 重新初始化。

        返回值
        如果成功,pthread_mutex_destroy() 返回 0。
        如果不成功,pthread_mutex_destroy() 返回 -1。

      互斥体
      显示互斥体如何用于线程同步的示例

      #include 
      #include 
      #include 
      #include 
      #include 
        
      pthread_t tid[2];
      int counter;
      pthread_mutex_t lock;
        
      void* trythis(void* arg)
      {
          pthread_mutex_lock(&lock);
        
          unsigned long i = 0;
          counter += 1;
          printf("\n Job %d has started\n", counter);
        
          for (i = 0; i < (0xFFFFFFFF); i++)
              ;
        
          printf("\n Job %d has finished\n", counter);
        
          pthread_mutex_unlock(&lock);
        
          return NULL;
      }
        
      int main(void)
      {
          int i = 0;
          int error;
        
          if (pthread_mutex_init(&lock, NULL) != 0) {
              printf("\n mutex init has failed\n");
              return 1;
          }
        
          while (i < 2) {
              error = pthread_create(&(tid[i]),
                                     NULL,
                                     &trythis, NULL);
              if (error != 0)
                  printf("\nThread can't be created :[%s]",
                         strerror(error));
              i++;
          }
        
          pthread_join(tid[0], NULL);
          pthread_join(tid[1], NULL);
          pthread_mutex_destroy(&lock);
        
          return 0;
      }
      

      在上面的代码中:

      • 互斥锁在主函数的开头初始化。
      • 在使用共享资源“计数器”时,相同的互斥锁在“trythis()”函数被锁定。
      • 在函数'trythis()' 结束时,相同的互斥锁被解锁。
      • 在主函数结束时,当两个线程都完成时,互斥锁被销毁。

      输出 :

      Job 1 started
      Job 1 finished
      Job 2 started
      Job 2 finished
      

      所以这次两个作业的开始和完成日志都存在。所以线程同步是通过使用互斥体来实现的。

      参考 :
      同步(计算机科学)
      锁(计算机科学)