Имеет ли здесь значение порядок разблокировки мьютексов?

предположим, у меня есть две переменные,protected_var1 и protected_var2. Предположим далее, что эти переменные обновляются через несколько потоков и довольно независимы в том, что обычно один или другой, но не оба работают - поэтому у них есть свой собственный мьютекс для эффективности.

предположим:

- Я всегда блокирую мьютексы в порядке (мьютекс1, затем мьютекс2) в моем коде в регионах, где требуются обе блокировки.

- оба мьютекса используются много другие места сами по себе (например, просто заблокировать мьютекс1 или просто заблокировать мьютекс2).

имеет ли порядок, в котором я разблокирую мьютексы в конце функции, используя оба, разницу в этой ситуации?

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);
}

Я задал вопрос давно в интервью относительно этой ситуации, и я вышел, чувствуя, что ответ был да - эти две разблокирует имеет значение. Я ни за что на свете не могу понять, как тупик может возникнуть из-за хотя если замки всегда получаются в том же порядке, где они используются.

9 ответов


Я не могу на всю жизнь понять, как тупик может возникнуть из-за этого, хотя, если замки всегда получаются в том же порядке, где оба используются.

в этих обстоятельствах я не думаю, что порядок разблокировки мьютексов может быть причиной тупика.

С pthread_mutex_unlock() не блокирует, оба мьютекса всегда будут разблокированы независимо от порядка двух вызовов.

обратите внимание, что если вы попытаетесь приобрести любой замки!--10-->между два вызова разблокировки, это может полностью изменить изображение.


заказ не должен иметь значения, если вы не пытаетесь приобрести еще один замок между выпусками. Главное всегда приобрести замки в том же порядке; в противном случае, вы рискуете в тупик.

EDIT:

чтобы развернуть ограничение: вы должны установить строгий порядок среди на мьютексы, например,mutex1 предваряет mutex2 (но это правило действует для любое количество мьютексов). Вы можете запросить блокировку мьютекса, только если не держите мьютекс который приходит после него в порядке; например, вы не можете запросить блокировку mutex1 Если вы держите замок на mutex2. В любое время эти правила соблюдаются, вы должны быть безопасными. Касаемо освобождение, если вы отпустите mutex1, затем попробуйте повторно получить его перед выпуская mutex2, вы нарушили правило. В связи с этим, возможно, некоторое преимущество в уважении стекового порядка: последний приобретенный всегда первый освобожденный. Но это своего рода косвенный эффект: правило заключается в том, что вы не можете запросить lock on mutex1 Если вы держите один на mutex2. Независимо от того, был ли у вас замок на mutex1 когда вы приобрел замок на mutex2 или нет.


Это не имеет значения для правильности замок. Причина в том, что, даже если предположить, что какой-то другой поток ждет блокировки мьютекс1, а затем мьютекс2, худший случай заключается в том, что он немедленно запланирован, как только вы отпустите мьютекс1 (и приобретете мьютекс1). Затем он блокирует ожидание mutex2, который поток, о котором вы спрашиваете, выпустит, как только он снова будет запланирован, и нет причин, по которым это не должно произойти в ближайшее время (немедленно, если это только два потока в играть.)

таким образом, может быть небольшая стоимость в производительности именно в такой ситуации, по сравнению с тем, если вы выпустили mutex2 первым, и поэтому была только одна операция переназначения. Ничего, что вы обычно ожидаете предсказать или беспокоиться, хотя все это находится в пределах "планирование часто не является детерминированным".

порядок освобождения блокировок, безусловно, может повлиять на планирование в целом. Предположим, что есть две нити, ожидающие вашего нить, и один из них заблокирован на mutex1, а другой заблокирован на mutex2. Может оказаться, что какой бы замок вы ни отпустили первым, этот поток запускается первым, просто потому, что ваш поток пережил свое приветствие (потребленное больше, чем весь временной срез), и, следовательно, получает descheduled, как только что-либо еще можно запустить. Но это не может вызвать ошибку в другой правильной программе: вам не разрешено полагаться на ваш поток descheduled, как только его отпускает первый замок. Таким образом, либо порядок этих двух потоков ожидания, работающих одновременно, если у вас несколько ядер, либо два чередующихся на одном ядре, должны быть одинаково безопасными, какой бы порядок вы ни отпустили блокировки.


порядок разблокировки не может привести к блокировкам. Однако, если дают возможность, рекомендую разблокировать их в обратном порядке блокировки. Это оказывает незначительное влияние на выполнение кода. Однако разработчики привыкли мыслить в терминах областей, а области "закрываются" в обратном порядке. Видя их в обратном порядке, просто подумайте о сканирующих замках. Это подводит меня ко второму пункту, который заключается в том, что в большинстве случаев безопаснее всего заменить прямые вызовы для блокировки и разблокировки stack based guard, который вызывает их для вас. Это дает наибольшую степень правильности для наименьших умственных усилий, а также оказывается безопасным при наличии исключений (которые действительно могут испортить ручную разблокировку)!

простой охранник (есть много там.. это просто быстрая самокрутки):

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
}

нет, это не важно. Взаимоблокировка не может быть результатом этого; оба оператора разблокировки гарантированно преуспеют (повреждение кучи бара или аналогичные проблемы).


приказом разблокировки здесь не проблема, но порядок замок может быть проблемой.

считаем:

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. Поэтому неплохо каким-то образом гарантировать, что вложенные блокировки мьютекса всегда блокируются в том же порядке.


Я вижу, что если другая операция принимает mutex2 и сохраняет его в течение длительного времени, ваша foo() функция застрянет после pthread_mutex_lock(&mutex1); и, вероятно, будет иметь некоторый хит производительности.


пока всякий раз, когда var1 и var2 заблокированы, они заблокированы в том же порядке, что и вы, независимо от порядка освобождения. На самом деле освобождение в том порядке, в котором они были заблокированы, - это поведение RAII, найденное в STL и BOOST locks.


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);
}

Я думаю, это может предотвратить затор