Какая разница между первой блокировкой и созданием lock_guard (accept_lock) и созданием unique_lock (defer_lock) и блокировки?

Я нашел следующие 2 фрагмента кода:

  • http://en.cppreference.com/w/cpp/thread/lock

    void assign_lunch_partner(Employee &e1, Employee &e2)                                                                                                  
    {   
        // use std::lock to acquire two locks without worrying about 
        // other calls to assign_lunch_partner deadlocking us
        {   
            // m is the std::mutex field
            std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
            std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
            std::lock(lk1, lk2);
            // ...
        }   
    }
    
  • http://www.amazon.com/C- Concurrency -Action-Practical-Multithreading/dp/1933988770

    void swap(X& lhs, X&rhs){                                                                                                                              
      if(&lhs == &rhs)
        return;
      // m is the std::mutex field
      std::lock(lhs.m, rhs.m);
      std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
      std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
      swap(lhs.some_detail, rhs.some_detail);
    }
    

Я хотел спросить, в чем разница и последствия использования любой из двух версий? (сначала заблокируйте или сначала создайте std::lock_guard или std::unique_lock?)

Ответ 1

1) Первый пример кода

{   
    static std::mutex io_mutex;
    std::lock_guard<std::mutex> lk(io_mutex);
    std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
}   

Это стандартная защита блокировки, когда область действия завершена, блокируется lk

{   
    std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
    std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
    std::lock(lk1, lk2);
    std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
    // ...
} 

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

{   
    std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
    std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
    std::lock(lk1);
    std::lock(lk2); // Risk of dedalock !
    std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
    // ...
} 

2) Второй пример кода

void swap(X& lhs, X&rhs){                                                                                                                              
  if(&lhs == &rhs)
    return;
  // m is the std::mutex field
  std::lock(lhs.m, rhs.m);
  std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
  std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
  swap(lhs.some_detail, rhs.some_detail);
}

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

Обратите внимание, что std::adopt_lock требует, чтобы текущий поток владел мьютексом (это так, потому что мы просто заблокировали их)


Заключение

Здесь есть 2 шаблона:

1) Заблокируйте оба мьютекса одновременно, затем создайте защитные устройства

2) Создайте защитные устройства, затем заблокируйте оба мьютекса одновременно

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

Что касается разницы между std::lock_guard и std::unique_lock, вы должны увидеть эту другую запись SO, большую часть времени std::lock_guard достаточно.

Ответ 2

На самом деле есть параграф (3.2.6) в книге, в котором объясняется, что код практически эквивалентен, и вы можете заменить его на другой. Единственное различие заключается в том, что std::unique_lock имеет тенденцию занимать больше места и меньше доли, чем std::lock_guard.

Нижняя строка - всякий раз, когда вам не нужна дополнительная гибкость, которую предоставляет std::unique_lock, перейдите к std::lock_guard.

Ответ 3

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

std::lock(lhs.m, rhs.m);
// <-- Bad news if someone adds junk here that can throw.
std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);

Также возможно случайно удалить/опустить одну из объявлений защиты без каких-либо ошибок времени компиляции. Проблема будет очевидна во время выполнения, когда срабатывает тупик, но неудобно отслеживать тупик до его источника.

Версия defer_lock не страдает одной из этих проблем. Поскольку объекты защиты объявляются до того, как произойдет блокировка, нет опасного окна. И, конечно, если вы опустите/удалите одну из объявлений охранника, вы получите ошибку компилятора при вызове std::lock.