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

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

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

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

5 ответов

Решение

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

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

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

Как упоминал @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 из-за блокировки того же мьютекса.

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

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%, вам нужно пересмотреть весь путь к коду, а затем пересмотреть его еще раз, посмотрев его снова...

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

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

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

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

Не знаю, какой 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
}

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

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

    //do transaction.
}
Другие вопросы по тегам