std :: unique_lock <std :: mutex> или std :: lock_guard <std :: mutex>?

349

У меня есть два варианта использования.

О. Я хочу синхронизировать доступ двумя потоками к очереди.

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

Для варианта использования AI см. Пример кода с использованием std::lock_guard<>. Для варианта использования BI см. Пример кода с использованием std::unique_lock<>.

В чем разница между этими двумя и какой я должен использовать в каком случае использования?

chmike
источник

Ответы:

344

Разница в том, что вы можете заблокировать и разблокировать std::unique_lock. std::lock_guardбудет заблокирован только один раз на строительстве и разблокирован при уничтожении.

Так что для варианта использования B вам определенно нужна std::unique_lockпеременная условия. В случае А это зависит от того, нужно ли вам снова заблокировать охрану.

std::unique_lockимеет другие функции, которые позволяют ему, например: создавать без немедленной блокировки мьютекса, но создавать оболочку RAII (см. здесь ).

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

class MyClass{
    std::mutex my_mutex;
    void member_foo() {
        std::lock_guard<mutex_type> lock(this->my_mutex);            
        /*
         block of code which needs mutual exclusion (e.g. open the same 
         file in multiple threads).
        */

        //mutex is automatically released when lock goes out of scope           
};

Для уточнения вопроса по chmike, по умолчанию std::lock_guardи std::unique_lockтак же. Таким образом, в приведенном выше случае, вы можете заменить std::lock_guardна std::unique_lock. Тем не менее, std::unique_lockможет иметь немного больше накладных расходов.

Обратите внимание, что в эти дни следует использовать std::scoped_lockвместо std::lock_guard.

Стефан Доллберг
источник
2
С помощью инструкции std :: unique_lock <std :: mutex> lock (myMutex); будет ли мьютекс заблокирован конструктором?
chmike
3
@chmike Да, так и будет. Добавлены некоторые разъяснения.
Стефан Доллберг
10
@chmike Ну, я думаю, это не столько вопрос эффективности, сколько функциональности. Если std::lock_guardдля вашего случая А этого достаточно, то вам следует его использовать. Это не только позволяет избежать ненужных накладных расходов, но также показывает читателю намерение, что вы никогда не разблокируете эту защиту.
Стефан Доллберг
5
@chmike: теоретически да. Однако Mutices не совсем легкие конструкции, поэтому дополнительные издержки unique_lock, вероятно, будут уменьшены стоимостью фактической блокировки и разблокировки мьютекса (если компилятор не оптимизировал эти издержки, что могло бы быть возможным).
Гризли
6
So for usecase B you definitely need a std::unique_lock for the condition variable- да, но только в потоке cv.wait()s, потому что этот метод атомарно освобождает мьютекс. В другом потоке, где вы обновляете общую переменную (переменные) и затем вызываете cv.notify_one(), lock_guardдостаточно просто заблокировать мьютекс в области видимости ... если вы не делаете ничего более сложного, что я не могу себе представить! например, en.cppreference.com/w/cpp/thread/condition_variable - у меня работает :)
underscore_d
115

lock_guardи unique_lockв значительной степени одно и то же; lock_guardявляется ограниченной версией с ограниченным интерфейсом.

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

Таким образом, вы всегда используете lock_guard, если вам не нужны возможности unique_lock. А condition_variableнужен unique_lock.

Себастьян Редл
источник
11
A condition_variable needs a unique_lock.- да, но только с той wait()стороны, как подробно изложено в моем комментарии к инф.
underscore_d
48

Используйте, lock_guardесли вам не нужно вручную unlockмьютекс между ними, не разрушая lock.

В частности, condition_variableразблокирует его мьютекс при переходе в режим ожидания при вызовах wait. Вот почему здесь lock_guardне достаточно.

ComicSansMS
источник
Передача lock_guard одному из методов ожидания условной переменной была бы хороша, потому что мьютекс всегда восстанавливается по окончании ожидания по любой причине. Однако стандарт предоставляет только интерфейс для unique_lock. Это можно рассматривать как недостаток в стандарте.
Крис Вайн
3
@Chris В этом случае вы все равно нарушите инкапсуляцию. Метод ожидания должен быть в состоянии извлечь мьютекс из lock_guardи разблокировать его, таким образом временно нарушая инвариант класса сторожа. Несмотря на то, что это происходит незаметно для пользователя, я считаю, что это законная причина для недопущения использования lock_guardв этом случае.
ComicSansMS
Если это так, это было бы невидимым и необнаружимым. GCC-4.8 делает это. wait (unique_lock <mutex> &) вызывает __gthread_cond_wait (& _ M_cond, __lock.mutex () -> native_handle ()) (см. libstdc ++ - v3 / src / c ++ 11 / condition_variable.cc), который вызывает pthread_cond_wait () (см. libgcc /gthr-posix.h). То же самое можно сделать для lock_guard (но это не так, потому что его нет в стандарте для condition_variable).
Крис Вайн
4
@Chris Суть в том, что lock_guardвообще не позволяет получить базовый мьютекс. Это намеренное ограничение, позволяющее более просто рассуждать о коде, который использует, lock_guardа не о коде, который использует unique_lock. Единственный способ добиться того, что вы просите, - это сознательно нарушить инкапсуляцию lock_guardкласса и подвергнуть его реализацию другому классу (в данном случае - condition_variable). Это сложная цена, чтобы заплатить за сомнительное преимущество пользователя условной переменной, которая не должна помнить разницу между двумя типами блокировки.
ComicSansMS
4
@Chris Откуда у тебя идея, которая condition_variable_any.waitбудет работать с lock_guard? Стандарт требует, чтобы предоставленный тип Замка соответствовал BasicLockableтребованию (§30.5.2), который lock_guardне делает. Только его основной мьютекс, но по причинам, которые я указал ранее, интерфейс lock_guardне обеспечивает доступ к мьютексу.
ComicSansMS
11

Есть некоторые общие вещи , между lock_guardи unique_lockи некоторые различия.

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

Это явление противоречит принципу lock_guard. lock_guardможет быть построен только один раз и разрушен только один раз.

Следовательно, lock_guardне может использоваться в комбинации с условной переменной, но unique_lockможет быть (потому что unique_lockможет быть заблокирован и разблокирован несколько раз).

Sandeep
источник
5
he compiler does not allow using a lock_guard in combination with a condition variableЭто неверно Это , конечно , это позволяет и работать отлично с lock_guardна notify()ИНГ стороне. Только wait()сторона int требует unique_lock, потому что wait()должна снять блокировку при проверке состояния.
underscore_d
0

Они на самом деле не одни и те же мьютексы, lock_guard<muType>имеют почти то же самое std::mutex, с той разницей, что их время жизни заканчивается в конце области (называемой D-tor), поэтому четкое определение этих двух мьютексов

lock_guard<muType> имеет механизм владения мьютексом во время блока с ограниченным пространством.

И

unique_lock<muType> является оберткой, допускающей отложенную блокировку, ограниченные по времени попытки блокировки, рекурсивную блокировку, передачу владения блокировкой и использование с переменными условия.

Вот пример реализации:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <chrono>

using namespace std::chrono;

class Product{

   public:

       Product(int data):mdata(data){
       }

       virtual~Product(){
       }

       bool isReady(){
       return flag;
       }

       void showData(){

        std::cout<<mdata<<std::endl;
       }

       void read(){

         std::this_thread::sleep_for(milliseconds(2000));

         std::lock_guard<std::mutex> guard(mmutex);

         flag = true;

         std::cout<<"Data is ready"<<std::endl;

         cvar.notify_one();

       }

       void task(){

       std::unique_lock<std::mutex> lock(mmutex);

       cvar.wait(lock, [&, this]() mutable throw() -> bool{ return this->isReady(); });

       mdata+=1;

       }

   protected:

    std::condition_variable cvar;
    std::mutex mmutex;
    int mdata;
    bool flag = false;

};

int main(){

     int a = 0;
     Product product(a);

     std::thread reading(product.read, &product);
     std::thread setting(product.task, &product);

     reading.join();
     setting.join();


     product.showData();
    return 0;
}

В этом примере я использовал unique_lock<muType>сcondition variable

rekkalmd
источник
-5

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

Кажется, нет никаких причин, по которым функции ожидания std :: condition_variable не должны принимать lock_guard, а также unique_lock, потому что всякий раз, когда ожидание заканчивается (по любой причине), мьютекс автоматически повторно запрашивается, чтобы не вызывать никакого семантического нарушения. Однако в соответствии со стандартом, чтобы использовать std :: lock_guard с условной переменной, вы должны использовать std :: condition_variable_any вместо std :: condition_variable.

Редактировать : удалено «Использование интерфейса pthreads std :: condition_variable и std :: condition_variable_any должны быть идентичны». Рассматривая реализацию gcc:

  • std :: condition_variable :: wait (std :: unique_lock &) просто вызывает pthread_cond_wait () для базовой переменной условия pthread по отношению к мьютексу, содержащемуся в unique_lock (и поэтому может в равной степени делать то же самое для lock_guard, но не потому, что стандарт не предусматривает этого)
  • std :: condition_variable_any может работать с любым блокируемым объектом, включая тот, который вообще не является блокировкой мьютекса (следовательно, он может работать даже с межпроцессным семафором)
Крис Вайн
источник