Пример повышения shared_mutex (несколько чтений / одна запись)?

116

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

Я думаю, что это то, что boost::shared_mutexнужно делать, но я не понимаю, как это использовать, и не нашел четкого примера.

Есть ли у кого-нибудь простой пример, который я мог бы использовать для начала?

kevin42
источник
Пример 1800 ИНФОРМАЦИЯ верен. См. Также эту статью: Что нового в Boost Threads .
Assaf Lavie,
возможный дубликат
блокировок

Ответы:

102

Похоже, вы бы сделали что-то вроде этого:

boost::shared_mutex _access;
void reader()
{
  // get shared access
  boost::shared_lock<boost::shared_mutex> lock(_access);

  // now we have shared access
}

void writer()
{
  // get upgradable access
  boost::upgrade_lock<boost::shared_mutex> lock(_access);

  // get exclusive access
  boost::upgrade_to_unique_lock<boost::shared_mutex> uniqueLock(lock);
  // now we have exclusive access
}
1800 ИНФОРМАЦИЯ
источник
7
Я впервые использую boost, и я новичок в C ++, так что, возможно, мне чего-то не хватает, но в моем собственном коде мне пришлось указать тип, например: boost :: shared_lock <shared_mutex> lock (_access);
Кен Смит
2
Я сам пытаюсь этим воспользоваться, но получаю сообщение об ошибке. отсутствующие аргументы шаблона перед блокировкой. Любые идеи?
Мэтт
2
@shaz Они имеют ограниченную область видимости, но вы можете выпустить их раньше с помощью .unlock (), если вам нужно.
mmocny
4
Я добавил недостающие аргументы шаблона.
1
@raaj, вы можете получить upgrade_lock, но при обновлении до уникального замка блокировка будет заблокирована до тех пор, пока не будет выпущен shared_lock
1800 ИНФОРМАЦИЯ
166

1800 ИНФОРМАЦИЯ более или менее верна, но есть несколько проблем, которые я хотел бы исправить.

boost::shared_mutex _access;
void reader()
{
  boost::shared_lock< boost::shared_mutex > lock(_access);
  // do work here, without anyone having exclusive access
}

void conditional_writer()
{
  boost::upgrade_lock< boost::shared_mutex > lock(_access);
  // do work here, without anyone having exclusive access

  if (something) {
    boost::upgrade_to_unique_lock< boost::shared_mutex > uniqueLock(lock);
    // do work here, but now you have exclusive access
  }

  // do more work here, without anyone having exclusive access
}

void unconditional_writer()
{
  boost::unique_lock< boost::shared_mutex > lock(_access);
  // do work here, with exclusive access
}

Также обратите внимание: в отличие от shared_lock, только один поток может получить upgrade_lock за один раз, даже если он не обновлен (что, как мне показалось, было неудобно, когда я столкнулся с ним). Итак, если все ваши читатели - условные писатели, вам нужно найти другое решение.

mmocny
источник
1
Просто чтобы прокомментировать «другое решение». Когда все мои читатели были условными писателями, я заставлял их всегда приобретать shared_lock, и когда мне нужно было обновиться, чтобы писать привилегии, я бы .unlock () блокировал читателя и получил новый unique_lock. Это усложнит логику вашего приложения, и теперь у других писателей есть возможность изменить состояние с момента первого чтения.
mmocny
8
Разве строка не должна boost::unique_lock< boost::shared_mutex > lock(lock);читать boost::unique_lock< boost::shared_mutex > lock( _access ); ?
Стив Уилкинсон 01
4
Последнее предостережение очень странное. Если только один поток может одновременно удерживать upgrade_lock, в чем разница между upgrade_lock и unique_lock?
Кен Смит
2
@Ken Мне не очень понятно, но преимущество upgrade_lock заключается в том, что он не блокирует, если в настоящее время получены некоторые shared_locks (по крайней мере, пока вы не обновитесь до уникального). Однако второй поток, который попытается получить upgrade_lock, заблокируется, даже если первый не обновился до уникального, чего я не ожидал.
mmocny
6
Это известная проблема с ускорением. Кажется, это решено в бета-версии boost 1.50: svn.boost.org/trac/boost/ticket/5516
Офек Шилон,
47

Начиная с C ++ 17 (VS2015) вы можете использовать стандарт для блокировок чтения-записи:

#include <shared_mutex>

typedef std::shared_mutex Lock;
typedef std::unique_lock< Lock > WriteLock;
typedef std::shared_lock< Lock > ReadLock;

Lock myLock;


void ReadFunction()
{
    ReadLock r_lock(myLock);
    //Do reader stuff
}

void WriteFunction()
{
     WriteLock w_lock(myLock);
     //Do writer stuff
}

Для более старой версии вы можете использовать boost с тем же синтаксисом:

#include <boost/thread/locks.hpp>
#include <boost/thread/shared_mutex.hpp>

typedef boost::shared_mutex Lock;
typedef boost::unique_lock< Lock >  WriteLock;
typedef boost::shared_lock< Lock >  ReadLock;
Йохай Тиммер
источник
5
Я бы тоже сказал typedef boost::unique_lock< Lock > WriteLock; typedef boost::shared_lock< Lock > ReadLock;.
vines
6
Не нужно включать весь thread.hpp. Если вам просто нужны замки, включите замки. Это не внутренняя реализация. Сведите количество включений к минимуму.
Йохай Тиммер
5
Определенно самая простая реализация, но я думаю, что это сбивает с толку называть мьютексы и блокировки как блокировки. Мьютекс - это мьютекс, а блокировка - это то, что поддерживает его в заблокированном состоянии.
Тим МБ
17

Просто чтобы добавить еще немного эмпирической информации, я исследовал всю проблему обновляемых блокировок и пример повышения shared_mutex (несколько чтений / одна запись)? - хороший ответ, добавляющий важную информацию о том, что только один поток может иметь upgrade_lock, даже если он не обновлен, что важно, поскольку это означает, что вы не можете перейти с общей блокировки на уникальную, не освободив сначала общую блокировку. (Это обсуждалось в другом месте, но самая интересная тема здесь http://thread.gmane.org/gmane.comp.lib.boost.devel/214394 )

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

  1. Поток, который ожидает unique_lock на shared_mutex, блокирует всех входящих читателей, они должны ждать запроса писателей. Это гарантирует, что читатели не умрут писателей голодом (однако я считаю, что писатели могут голодать читателей).

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

Это важный вопрос для рассмотрения и, вероятно, должен быть задокументирован.

Джим Моррис
источник
3
Это Terekhov algorithmгарантирует 1., что писатель не может морить читателей голодом. Смотрите это . Но 2.верно. Upgrade_lock не гарантирует справедливости. Смотрите это .
JonasVautherin
2

Используйте семафор со счетчиком, равным количеству читателей. Пусть каждый читатель сделает один отсчет семафора для чтения, чтобы все они могли читать одновременно. Затем позвольте писателю взять ВСЕ счетчики семафоров перед записью. Это заставляет писатель ждать завершения всех чтений, а затем блокировать чтение во время записи.

R Virzi
источник
(1) Как сделать так, чтобы писатель уменьшал счет на произвольную величину атомарно ? (2) Если писатель каким-то образом уменьшает счет до нуля, как он будет ждать, пока уже запущенные читатели закончатся, прежде чем писать?
Ofek Shilon
Плохая идея: если два писателя попытаются получить доступ одновременно, вы можете зайти в тупик.
Caduchon 08
2

Отличный отклик Джима Морриса, я наткнулся на это, и мне потребовалось время, чтобы понять. Вот простой код, который показывает, что после отправки «запроса» на повышение unique_lock (версия 1.54) все запросы shared_lock блокируются. Это очень интересно, поскольку мне кажется, что выбор между unique_lock и upgradeable_lock позволяет нам, хотим ли мы иметь приоритет записи или нет.

Также (1) в сообщении Джима Морриса, похоже, противоречит этому: Boost shared_lock. Читать предпочитаете?

#include <iostream>
#include <boost/thread.hpp>

using namespace std;

typedef boost::shared_mutex Lock;
typedef boost::unique_lock< Lock > UniqueLock;
typedef boost::shared_lock< Lock > SharedLock;

Lock tempLock;

void main2() {
    cout << "10" << endl;
    UniqueLock lock2(tempLock); // (2) queue for a unique lock
    cout << "11" << endl;
    boost::this_thread::sleep(boost::posix_time::seconds(1));
    lock2.unlock();
}

void main() {
    cout << "1" << endl;
    SharedLock lock1(tempLock); // (1) aquire a shared lock
    cout << "2" << endl;
    boost::thread tempThread(main2);
    cout << "3" << endl;
    boost::this_thread::sleep(boost::posix_time::seconds(3));
    cout << "4" << endl;
    SharedLock lock3(tempLock); // (3) try getting antoher shared lock, deadlock here
    cout << "5" << endl;
    lock1.unlock();
    lock3.unlock();
}
dale1209
источник
На самом деле мне сложно понять, почему приведенный выше код блокируется, когда код в [ stackoverflow.com/questions/12082405/… работает.
dale1209
1
Фактически он заходит в тупик в (2), а не в (3), потому что (2) ждет, пока (1) освободит свою блокировку. Помните: чтобы получить уникальную блокировку, вам нужно дождаться завершения всех существующих общих блокировок.
JonasVautherin
@JonesV, даже если (2) ожидает завершения всех общих блокировок, это не будет тупиковой ситуацией, потому что это другой поток, чем тот, который получил (1), если строка (3) не существует, программа будет закончить без тупиков.
SagiLow 05