Имеет ли значение порядок разблокировки мьютексов здесь? - PullRequest
8 голосов
/ 23 февраля 2012

Допустим, у меня есть две переменные, protected_var1 и protected_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);
}

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

Ответы [ 9 ]

11 голосов
/ 23 февраля 2012

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

РЕДАКТИРОВАТЬ:

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

7 голосов
/ 23 февраля 2012

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

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

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

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

3 голосов
/ 23 февраля 2012

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

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

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

1 голос
/ 04 сентября 2013

Порядок разблокировки не может вызвать взаимоблокировки. Тем не менее, если дать возможность, я рекомендую разблокировать их в обратном порядке блокировки. Это незначительно влияет на выполнение кода. Тем не менее, разработчики привыкли мыслить с точки зрения областей, а области «закрываются» в обратном порядке. Видя их в обратном порядке, просто подумайте о замках. Это подводит меня ко второму пункту, который заключается в том, что в большинстве случаев безопаснее заменить прямые вызовы блокировки и разблокировки защитой на основе стека, которая вызывает их для вас. Это дает наибольшую степень корректности при наименьших умственных усилиях, а также оказывается безопасным при наличии исключений (которые могут действительно испортить ручную разблокировку)!

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

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
}
0 голосов
/ 23 сентября 2016
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);
}

Полагаю, это может предотвратить тупик

0 голосов
/ 23 февраля 2012

Пока блокировок var1 и var2 заблокированы, они блокируются в том же порядке, что и вы, независимо от порядка освобождения. Фактически освобождение в том порядке, в котором они были заблокированы, является поведением освобождения RAII, обнаруженным в блокировках STL и BOOST.

0 голосов
/ 23 февраля 2012

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

0 голосов
/ 23 февраля 2012

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

Рассмотрим:

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

0 голосов
/ 23 февраля 2012

Нет, это не имеет значения.Это не может привести к тупику;оба оператора разблокировки гарантированно успешно (повреждение кучи бар или аналогичные проблемы).

...