Включение std :: lock_guard в дополнительную область - программирование

Включение std :: lock_guard в дополнительную область

Есть ли смысл делать что-то вроде помещения std::lock_guard в дополнительную область видимости, чтобы период блокировки был как можно короче?

Псевдокод:

// all used variables beside the lock_guard are created and initialized somewhere else
...// do something

{ // open new scope
    std::lock_guard<std::mutex> lock(mut);
    shared_var = newValue;  
} // close the scope

... // do some other stuff (that might take longer)

Есть ли еще преимущества, кроме короткой продолжительности блокировки?

Какие могут быть негативные побочные эффекты?

4b9b3361

Ответ 1

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

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

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

Может быть еще один момент для рассмотрения (у меня нет достаточного практического опыта, чтобы говорить с уверенностью). Блокировка/освобождение мьютекса потенциально может быть операцией с нетривиальными издержками производительности. Следовательно, может оказаться, что сохранение блокировки в течение немного более длительного периода вместо разблокировки и повторной блокировки несколько раз в течение одной операции может фактически улучшить общую производительность. Это то, что профилирование может показать вам.

Ответ 2

Там может быть недостаток: вы не можете защитить инициализации таким образом. Например:

{
    std::lock_guard<std::mutex> lock(mut);
    Some_resource var{shared_var};
} // oops! var is lost

Вы должны использовать назначение, как это:

Some_resource var;
{
    std::lock_guard<std::mutex> lock(mut);
    var = shared_Var;
}

что может быть неоптимальным для некоторых типов, для которых инициализация по умолчанию с последующим назначением менее эффективна, чем непосредственная инициализация. Более того, в некоторых ситуациях вы не можете изменить переменную после инициализации. (например, const переменные)


user32434999 указал на это решение:

// use an immediately-invoked temporary lambda
Some_resource var {
    [&] {
        std::lock_guard<std::mutex> lock(mut);
        return shared_var;
    } () // parentheses for invoke
};

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

Ответ 3

Да, это имеет смысл.

Других преимуществ нет, побочных эффектов нет (это хороший способ написать это).

Еще лучший способ - извлечь ее в приватную функцию-член (если у вас есть операция, которая синхронизируется таким образом, вы могли бы также дать операции ее собственное имя):

{
    // all used variables beside the lock_guard are created and initialized somewhere else
    ...// do something

    set_var(new_value);

    ... // do some other stuff (that might take longer)
}

void your_class::set_value(int new_value)
{
    std::lock_guard<std::mutex> lock(mut);
    shared_var = new_value;
}

Ответ 4

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

Я вижу еще один момент, который не был упомянут в других ответах: транзакционные операции. Давайте воспользуемся классическим примером перевода денег между двумя банковскими счетами. Чтобы ваша банковская программа была правильной, изменение баланса двух банковских счетов должно быть выполнено без разблокировки мьютекса между ними. В противном случае, другой поток мог бы заблокировать мьютекс, пока программа находится в странном состоянии, когда только одна из учетных записей была зачислена/списана, в то время как баланс другой учетной записи остался нетронутым!

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

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

Если по какой-то причине удержание мьютекса заблокированным на протяжении всей транзакции недопустимо, вы можете использовать следующий алгоритм:
1. Блокировка мьютекса, чтение входных данных, разблокировка мьютекса.
2. Выполните все необходимые вычисления, сохраните результаты локально.
3. Заблокируйте мьютекс, убедитесь, что входные данные не изменились, выполните транзакцию с легкодоступными результатами, разблокируйте мьютекс.

Если входные данные изменились во время выполнения шага 2, отбросьте результаты и начните заново со свежих входных данных.

Ответ 5

Я не вижу причин для этого. Если вы делаете что-то такое простое, как "установить одну переменную" - используйте atomic <> и вам вообще не нужны мьютекс и блокировка. Если вы делаете что-то сложное - извлеките этот код в новую функцию и используйте блокировку в первой строке.