【问题标题】:Does order of unlocking mutexes make a difference here?解锁互斥锁的顺序在这里有什么不同吗?
【发布时间】:2012-02-23 14:20:15
【问题描述】:

假设我有两个变量,protected_var1protected_var2。让我们进一步假设这些变量是通过多个线程更新的,并且是相当独立的,因为通常一个或另一个而不是两者都工作 - 所以它们都有自己的互斥锁以提高效率。

假设:

-在需要两个锁的区域,我总是在代码中按顺序锁定互斥锁(mutex1 然后 mutex2)。

-这两个互斥体都被它们自己在许多其他地方使用(比如锁定 mutex1,或者只是锁定 mutex2)。

在这种情况下,我在函数结束时解锁互斥锁的顺序是否会有所不同?

void foo()
{
    pthread_mutex_lock(&mutex1);
    pthread_mutex_lock(&mutex2);

    int x = protected_var1 + protected_var2;

    pthread_mutex_unlock(&mutex1); //Does the order of the next two lines matter?
    pthread_mutex_unlock(&mutex2);
}

很久以前,我在一次采访中被问到一个关于这种情况的问题,我出来后觉得答案是肯定的——这两个解锁的顺序很重要。我一生都无法弄清楚死锁是如何导致的,尽管如果在使用两者的地方总是以相同的顺序获得锁。

【问题讨论】:

  • ...但是在它们没有同时使用的情况下,两个线程可能会相互死锁。为了绝对安全,您应该以相同的顺序锁定两者。是的,它可能会序列化你不需要的东西,但否则你必须确保没有单独锁定的线程可能与它们都被锁定的地方发生冲突。

标签: c++ multithreading pthreads deadlock


【解决方案1】:

顺序无关紧要,只要您不尝试获取 版本之间的另一个锁。重要的是永远 以相同的顺序获取锁;否则,您将面临陷入僵局的风险。

编辑:

要扩展约束:您必须在它们之间建立严格的排序 互斥体,例如mutex1mutex2 之前(但此规则适用于 任意数量的互斥体)。您只能请求锁定互斥锁,如果您 不要持有按顺序排列在它之后的互斥锁;例如你不可以 如果您持有mutex2 的锁定,则请求锁定mutex1。随时 这些规则得到尊重,你应该是安全的。关于 释放,如果你释放mutex1,然后尝试重新获取它之前 释放mutex2,你违反了规则。在这方面,可能有 在尊重类似堆栈的顺序方面有一些优势:最后获得的是 总是第一个发布。但这是一种间接的影响: 规则是,如果您持有mutex1,则不能请求锁定 mutex2。不管你有没有锁定mutex1 是否获得了mutex2 的锁定。

【讨论】:

  • 非常非常重要的一点。我知道解锁订购是有原因的。请稍微扩展一下,以便人们看到它。考虑解锁mutex1,做一些事情,然后再次抓住mutex1。我记得有一个死锁是通过反向排序互斥锁来解决的,这就是原因。
  • @D.Shawley 这导致死锁。 (我似乎记得在最近的回答中更详细地分析了这一点,关于在容器 容器中的单个元素上使用互斥锁的问题。)但这违反了排序约束:如果你获得两个互斥体,你必须先获得mutex1
【解决方案2】:

如果在使用两者时总是以相同的顺序获得锁,我终生无法弄清楚死锁是如何导致的。

在这种情况下,我认为解锁互斥锁的顺序不会导致死锁。

由于pthread_mutex_unlock() 不会阻塞,因此无论两个调用的顺序如何,两个互斥锁总是会解锁。

请注意,如果您尝试在两个解锁调用之间获取任何锁,这可能会完全改变图片。

【讨论】:

    【解决方案3】:

    锁定的正确性无关紧要。原因是,即使假设某个其他线程正在等待锁定 mutex1,然后是 mutex2,最坏的情况是它会在您释放 mutex1(并获取 mutex1)后立即被调度。然后它阻塞等待 mutex2,您所询问的线程将在它再次被安排后立即释放,并且没有理由不应该很快发生(立即,如果这些是唯一的两个线程)。

    因此,在那种确切的情况下,与您首先释放 mutex2 相比,性能可能会有很小的成本,因此只有一次重新调度操作。不过,您通常不会期望预测或担心,这一切都在“调度通常不是确定性的”的范围内。

    不过,您释放锁的顺序肯定会影响总体调度。假设有两个线程在等待你的线程,其中一个在 mutex1 上被阻塞,而另一个在 mutex2 上被阻塞。结果可能是,无论您首先释放哪个锁,该线程都会首先运行,这仅仅是因为您的线程已经超过了它的受欢迎程度(消耗的时间超过了整个时间片),因此一旦其他任何东西都可以运行,就会被取消调度。但这不会在其他正确的程序中导致错误:您不能依赖您的线程在释放第一个锁后立即被取消调度。因此,这两个等待线程的运行顺序(如果您有多个内核则同时运行,或者两个在一个内核上交替运行)必须同样安全,无论您释放锁的顺序是什么。

    【讨论】:

      【解决方案4】:

      解锁顺序不会导致死锁。但是,如果有机会,我建议以反向锁定顺序解锁它们。这对代码的运行影响可以忽略不计。但是,开发人员习惯于根据范围进行思考,并且范围以相反的顺序“关闭”。以相反的顺序查看它们以简单地想到作用域锁。这让我想到了第二点,即在大多数情况下,将直接调用 lock 和 unlock 替换为为您调用它们的基于堆栈的守卫是最安全的。这样做可以以最少的脑力劳动获得最大程度的正确性,并且在出现异常的情况下也恰好是安全的(手动解锁真的很糟糕)!

      一个简单的守卫(那里有很多......这只是一个快速滚动你自己的):

      class StMutexLock
      {
          public:
              StMutexLock(pthread_mutex_t* inMutex)
              : mMutex(inMutex)
              {
                  pthread_mutex_lock(mMutex);
              }
      
              ~StMutexUnlock()
              {
                  pthread_mutex_unlock(mMutex);
              }
          private:
              pthread_mutex_t*   mMutex;
      }
      
      {
          StMutexLock lock2(&mutex2);
          StMutexLock lock1(&mutex1);
      
          int x = protected_var1 + protected_var2;
          doProtectedVar1ThingThatCouldThrow(); // exceptions are no problem!
          // no explicit unlock required.  Destructors take care of everything
      }
      

      【讨论】:

        【解决方案5】:

        不,没关系。这不会导致死锁;两个解锁运算符都保证成功(条堆损坏或类似问题)。

        【讨论】:

          【解决方案6】:

          这里解锁的顺序没有问题,但是锁定的顺序可能有问题。

          考虑:

          void foo()
          {
              pthread_mutex_lock(&mutex1);
              pthread_mutex_lock(&mutex2);
          
              int x = protected_var1 + protected_var2;
          
              pthread_mutex_unlock(&mutex1);
              pthread_mutex_unlock(&mutex2);
          }
          
          void bar()
          {
              pthread_mutex_lock(&mutex2);
              pthread_mutex_lock(&mutex1);
          
              int x = protected_var1 + protected_var2;
          
              pthread_mutex_unlock(&mutex1);
              pthread_mutex_unlock(&mutex2);
          }
          

          这可能导致死锁,因为 foo 可能已锁定 mutex1,现在正在等待 mutex2,而 bar 已锁定 mutex2,现在正在等待 mutex1。因此,以某种方式确保嵌套的互斥锁始终以相同的顺序锁定是个好主意。

          【讨论】:

            【解决方案7】:

            我可以看到,如果另一个操作占用mutex2 并保持很长时间,您的foo() 函数将在pthread_mutex_lock(&mutex1); 之后卡住,并且可能会对性能造成一些影响。

            【讨论】:

              【解决方案8】:

              只要 var1 和 var2 被锁定,它们以相同的顺序被锁定,无论释放顺序如何,您都是安全的。事实上,按锁定顺序释放是 STL 和 BOOST 锁中的 RAII 释放行为。

              【讨论】:

                【解决方案9】:
                void * threadHandle (void *arg) 
                {
                   // Try to lock the first mutex...
                   pthread_mutex_lock (&mutex_1);
                
                   // Try to lock the second mutex...
                   while (pthread_mutex_trylock(&mutex_2) != 0)  // Test if already locked   
                   {
                      // Second mutex is locked by some other thread.  Unlock the first mutex so that other threads won't starve or deadlock
                      pthread_mutex_unlock (&mutex_1);  
                
                      // stall here
                      usleep (100);
                
                      // Try to lock the first mutex again
                      pthread_mutex_lock(&mutex_1);
                   }
                
                   // If you are here, that means both mutexes are locked by this thread
                
                   // Modify the global data
                   count++;
                
                   // Unlock both mutexes!!!
                
                   pthread_mutex_unlock (&mutex_1);
                
                   pthread_mutex_unlock (&mutex_2);
                }
                

                我想这可以防止死锁

                【讨论】:

                • 除代码外,请提供一些详细信息,说明您的代码为何有效以及您为使其有效所做的工作。
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2021-11-23
                • 2010-11-18
                • 1970-01-01
                • 2018-05-23
                • 1970-01-01
                • 2023-03-15
                • 2010-11-22
                相关资源
                最近更新 更多