Получите блокировку двух мьютексов и избегайте взаимоблокировки

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

void foo::copy(const foo & rhs)
{
    pMutex->lock();
    rhs.pMutex->lock();
    // do copy
}

Foo имеет контейнер STL, а «копирование» по существу состоит из использования std::copy. Как заблокировать оба мьютекса, не вводя тупиковую ситуацию?


person pomeroy    schedule 12.02.2010    source источник
comment
std::lock имеет алгоритм предотвращения взаимоблокировок, передающий ему оба мьютекса, и он будет более удобочитаемым для других, чем реализация вашего собственного.   -  person Mellester    schedule 17.02.2019


Ответы (6)


Наложите какой-то общий порядок на экземпляры foo и всегда получайте их блокировки либо в возрастающем, либо в убывающем порядке, например,, foo1->lock(), а затем foo2->lock().

Другой подход заключается в использовании функциональной семантики и написании вместо этого метода foo::clone, который создает новый экземпляр, а не затирает существующий.

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

person Greg Bacon    schedule 12.02.2010
comment
Даже что-то такое простое, как адреса this vs rhs, будет работать. всегда сначала блокируйте тот, у которого меньший адрес. - person Kyle Butt; 12.02.2010
comment
clone работал бы хорошо, только если бы не копировал, и я не думаю, что неявное совместное использование будет работать, но я посмотрю. Интересный подход Кайла. Я не вижу недостатков. - person pomeroy; 12.02.2010
comment
это хорошее решение, чтобы предложить ему сделать временную копию данных. Однако std::lock уже предлагает такой алгоритм предотвращения взаимоблокировок. и было бы читабельнее - person Mellester; 17.02.2019

Как насчет этого?

void foo::copy(const foo & rhs)
{
    scopedLock lock(rhs.pMutex); // release mutex in destructor
    foo tmp(rhs);
    swap(tmp); // no throw swap locked internally
}

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

person Shing Yip    schedule 12.02.2010

Как упоминал @Mellester, вы можете использовать std::lock для блокировки нескольких мьютексов, избегая взаимоблокировки.

#include <mutex>

void foo::copy(const foo& rhs)
{
    std::lock(pMutex, rhs.pMutex);

    std::lock_guard<std::mutex> l1(pMutex, std::adopt_lock);
    std::lock_guard<std::mutex> l2(rhs.pMutex, std::adopt_lock);

    // do copy
}

Но обратите внимание, чтобы проверить, что rhs не является *this, поскольку в этом случае std::lock приведет к UB из-за блокировки того же мьютекса.

person 4xy    schedule 12.10.2020
comment
По какой причине вы бы не сделали std::unique_lock<std::mutex> сначала с std::defer_lock, а затем выполнили std::lock(l1,l2);? Или это просто личные предпочтения/стиль? - person Gizmo; 23.04.2021

это известная проблема, уже есть стандартное решение. std::lock() можно вызывать для 2 или более мьютексов одновременно, избегая взаимоблокировок. Дополнительная информация здесь содержит рекомендацию.

std::scoped_lock предлагает оболочку RAII для этой функции и обычно предпочтительнее прямого вызова std::lock.

конечно, на самом деле это не позволяет ранние выпуски одной блокировки над другой, поэтому используйте std::defer_lock или std::adopt_lock, как я сделал в этом ответе на аналогичный вопрос.

person Mellester    schedule 16.02.2019

Чтобы избежать взаимоблокировки, вероятно, лучше всего подождать, пока оба ресурса не будут заблокированы:

Не знаю, какой API мьютекса вы используете, поэтому вот какой-то произвольный псевдокод, предположим, что can_lock() только проверяет, может ли он заблокировать мьютекс, и что try_lock() возвращает true, если он заблокировал, и false, если мьютекс уже заблокирован кем-то еще .

void foo::copy(const foo & rhs)
{
    for(;;)
    {
        if(! pMutex->cany_lock() || ! rhs.pMutex->cany_lock())
        {
            // Depending on your environment call or dont call sleep()
            continue;
        }
        if(! pMutex->try_lock())
            continue;
        if(! rhs.pMutex->try_lock())
        {
            pMutex->try_lock()
            continue;
        }
        break;
    }
    // do copy
}
person RED SOFT ADAIR    schedule 12.02.2010
comment
Чтобы избежать взаимоблокировки, лучше всего ввести livelock? И крутить, используя 100% ЦП? - person bk1e; 13.02.2010

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

void Transfer(Receiver recv, Sender send)
{
    scoped_lock rlock(recv.mutex);
    scoper_lock slock(send.mutex);

    //do transaction.
}
person Naveen    schedule 22.12.2010
comment
это рецепт катастрофы - person Mellester; 17.02.2019