Может ли scoped_lock заблокировать shared_mutex в режиме чтения?

В С++ 17 введены как std::shared_mutex и std::scoped_lock. Моя проблема сейчас в том, что кажется, что scoped_lock блокирует общий мьютекс всегда в режиме монопольного scoped_lock (записи), когда он передается в качестве аргумента, а не в режиме общего доступа (чтения). В моем приложении мне нужно обновить объект dst с данными из объекта src. Я хочу заблокировать src shared и dst exclusive. К сожалению, это может привести к взаимоблокировке, если одновременно происходит вызов другого метода обновления с переключенными src и dst. Поэтому я хотел бы использовать причудливые механизмы предотвращения тупиковых ситуаций в std::scoped_lock.

Я мог бы использовать scoped_lock для блокировки как src и dst в монопольном режиме, но эта неоправданно строгая блокировка снижает производительность в других местах. Тем не менее, кажется, что можно обернуть src shared_mutex в std::shared_lock и использовать его с scoped_lock: когда scoped_lock во время действия блокировки вызывает try_lock() для shared_lock, последний на самом деле вызовет try_shared_lock() для src shared_mutex, и это то, что мне нужно.

Так что мой код выглядит так просто:

struct data {
    mutable std::shared_mutex mutex;
    // actual data follows
};

void update(const data& src, data& dst)
{
    std::shared_lock slock(src.mutex, std::defer_lock);
    std::scoped_lock lockall(slock, dst.mutex);
    // now can safely update dst with src???
}

Безопасно ли использовать (совместно используемый) защитный кожух, как этот, внутри другого защитного кожуха?

Ответы

Ответ 1

Как указывали различные комментаторы, которые читали код реализации стандартной библиотеки C++: да, использование std::shared_mutex заключенного в std::shared_lock() качестве одного из аргументов std::scoped_lock() безопасно.

По сути, std::shared_lock перенаправляет все вызовы lock() в lock_shared() в lock_shared().

std::shared_lock::lock -----------> mutex()->lock_shared(). // same for try_lock etc..

Другое возможное решение

std::shared_lock lk1(src.mutex, std::defer_lock);
std::unique_lock lk2(dst.mutex, std::defer_lock);
std::lock(lk1, lk2);

std::lock - это функция, которая принимает любое количество объектов Lockable и блокирует их все (или отменяет за исключением, в этом случае все они будут разблокированы).

std::scoped_lock соответствии с cppreference - это оболочка для std::lock с добавленной функциональностью вызова unlock() для каждого объекта Lockable в его деструкторе. Эта дополнительная функциональность здесь не требуется, так как std::shared_lock lk1 и std::unique_lock lk2 также работают в качестве std::unique_lock lk2 блокировки, которые разблокируют свои мьютексы, когда они выходят из области видимости.

Изменение: различные уточнения

Ответ 2

Mutex: добавьте безопасность потоков к общим ресурсам
Блокировка: добавьте RAII (и, возможно, дополнительную функциональность) в мьютекс

Различные блокировки позволяют блокировать мьютекс разными способами:

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

scoped_lock - это эксклюзивный замок с голой костью, который блокируется при его создании и разблокируется при его разрушении. unique_lock и shared_lock - это эксклюзивные и общие блокировки соответственно, которые также блокируются и разблокируются с помощью конструктора и деструктора по умолчанию. Тем не менее, они также обеспечивают дополнительную функциональность. Например, вы можете попытаться везти их, или вы можете разблокировать их, прежде чем они будут уничтожены.

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