Заблокировать несколько читателей, один писатель

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

Есть два способа исправить это:

  1. использовать TMultiReadExclusiveWriteSynchronizer
  2. избавьтесь от любых блокировок, используя подход без блокировок

Для 2. У меня пока есть следующее (любой код, который не имеет значения, был исключен):

type
  TDataManager = class
  private
    FAccessCount: integer;
    FData: TDataClass;
  public
    procedure Read(out _Some: integer; out _Data: double);
    procedure Write(_Some: integer; _Data: double);
  end;

procedure TDataManager.Read(out _Some: integer; out _Data: double);
var
  Data: TDAtaClass;
begin
  InterlockedIncrement(FAccessCount);
  try
    // make sure we get both values from the same TDataClass instance
    Data := FData;
    // read the actual data
    _Some := Data.Some;
    _Data := Data.Data;
  finally
    InterlockedDecrement(FAccessCount);
  end;
end;

procedure TDataManager.Write(_Some: integer; _Data: double);
var
  NewData: TDataClass;
  OldData: TDataClass;
  ReaderCount: integer;
begin
  NewData := TDataClass.Create(_Some, _Data);
  InterlockedIncrement(FAccessCount);
  OldData := TDataClass(InterlockedExchange(integer(FData), integer(NewData));
  // now FData points to the new instance but there might still be
  // readers that got the old one before we exchanged it.
  ReaderCount := InterlockedDecrement(FAccessCount);
  if ReaderCount = 0 then
    // no active readers, so we can safely free the old instance
    FreeAndNil(OldData)
  else begin
    /// here is the problem
  end;
end;

К сожалению, существует небольшая проблема - избавиться от экземпляра OldData после его замены. Если в методе Read в настоящее время нет другого потока (ReaderCount = 0), его можно безопасно удалить, и все. Но что мне делать, если это не так? Я мог бы просто сохранить его до следующего вызова и утилизировать его там, но планирование Windows теоретически может позволить потоку чтения спать, пока он находится в методе Read и все еще имеет ссылку на OldData.

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

В случае, если это имеет значение: я использую Delphi 2007 со встроенным диспетчером памяти. Я знаю, что диспетчер памяти, вероятно, в любом случае применяет некоторую блокировку при создании нового класса, но я хочу проигнорировать это на данный момент.

Изменить: возможно, из приведенного выше не было ясно: на протяжении всего времени существования объекта TDataManager существует только один поток, который записывает данные, а не несколько, которые могут конкурировать за доступ для записи. Так что это частный случай MREW.


person dummzeuch    schedule 26.05.2009    source источник
comment
Я с осторожностью отношусь к написанному самим собой безблокировочному коду, исправить его практически невозможно. Что касается TMREWS: нет никакого способа изменить время вашего варианта использования на типичных машинах, поскольку есть разные способы их реализации, а VCL дает вам только один. Для статьи, сравнивающей различные реализации (включая время), см. codeproject.com/KB/threads/testing_rwlocks .aspx   -  person mghie    schedule 26.05.2009


Ответы (4)


Я не знаю ни одного подхода MREW без блокировки (или микроблокировки, как в приведенном выше примере), который можно было бы реализовать в коде Intel86.

Для небольших (быстро истекающих) блокировок вращающийся подход из OmniThreadLibrary работает нормально:

type
TOmniMREW = record
strict private
  omrewReference: integer;      //Reference.Bit0 is 'writing in progress' flag
public
  procedure EnterReadLock; inline;
  procedure EnterWriteLock; inline;
  procedure ExitReadLock; inline;
  procedure ExitWriteLock; inline;
end; { TOmniMREW }

procedure TOmniMREW.EnterReadLock;
var
  currentReference: integer;
begin
  //Wait on writer to reset write flag so Reference.Bit0 must be 0 than increase Reference
  repeat
    currentReference := omrewReference AND NOT 1;
  until currentReference = InterlockedCompareExchange(omrewReference, currentReference + 2, currentReference);
end; { TOmniMREW.EnterReadLock }

procedure TOmniMREW.EnterWriteLock;
var
  currentReference: integer;
begin
  //Wait on writer to reset write flag so omrewReference.Bit0 must be 0 then set omrewReference.Bit0
  repeat
    currentReference := omrewReference AND NOT 1;
  until currentReference = InterlockedCompareExchange(omrewReference, currentReference + 1, currentReference);
  //Now wait on all readers
  repeat
  until omrewReference = 1;
end; { TOmniMREW.EnterWriteLock }

procedure TOmniMREW.ExitReadLock;
begin
  //Decrease omrewReference
  InterlockedExchangeAdd(omrewReference, -2);
end; { TOmniMREW.ExitReadLock }

procedure TOmniMREW.ExitWriteLock;
begin
  omrewReference := 0;
end; { TOmniMREW.ExitWriteLock }

Я только что заметил здесь возможную проблему с выравниванием - код должен проверять, что omrewReference выровнен по 4. Извещу автора.

person gabr    schedule 26.05.2009
comment
Если я не ошибаюсь, вы сами сообщите об этом ;-) Между прочим, неплохая библиотека. - person Davy Landman; 26.05.2009
comment
@gabr: Для многоядерных систем это очень хорошая вещь в наборе инструментов, +1. Тем не менее, у него есть один аспект с тонкой блокировкой R / W, представленной в Vista: доступ не может быть повышен с чтения до записи. Если я правильно прочту этот код, это приведет к бесконечному циклу. Может быть, стоит добавить заметку по этому поводу. - person mghie; 26.05.2009
comment
@Davy: Нет, я не автор, это GJ - парень, который также написал lock-free (или, скорее, микрозамковку) стек и очередь. - person gabr; 26.05.2009
comment
@DavyLandman, у меня был такой же вопрос, когда я читал, что габр сказал :) - person Edwin Yip; 13.09.2013
comment
@mghie Вы правы, этот замок будет крутиться вечно. Соответствующий тонкий замок для чтения / записи Windows будет вращаться только 1024 раза. После этого он вернется к быстрому мьютексу в пользовательском пространстве; недокументированное ключевое событие. - person Ian Boyd; 29.10.2013

Просто дополнение - то, что вы здесь видите, обычно известно как Указатели опасности. Понятия не имею, можно ли сделать что-то подобное в Delphi.

person Nikolai Fetissov    schedule 26.05.2009

Прошло некоторое время с тех пор, как я запачкал руки в Delphi, поэтому проверьте это перед использованием, но ... из памяти вы можете получить поведение с подсчетом ссылок, если вы используете интерфейс и реализацию с использованием TInterfacedObject.

type
    IDataClass = interface
        function GetSome: integer;
        function GetData: double;

        property Some: integer read GetSome;
        property Data: double read GetData;
    end;

    TDataClass = class(TInterfacedObject, IDataClass)
    private
        FSome: integer;
        FData: double;
    protected
        function GetSome: integer;
        function GetData: double;
    public
        constructor Create(ASome: integer; AData: double);
    end;

Затем вместо этого вы делаете все свои переменные типа ISomeData (смешивание ISomeData и TSomeData - очень плохая идея ... вы легко получаете проблемы со счетом ссылок).

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

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

person jerryjvl    schedule 27.05.2009
comment
К сожалению, подсчет ссылок для интерфейсов не является потокобезопасным. - person dummzeuch; 27.05.2009
comment
Подсчет ссылок является потокобезопасным. Совместное использование одной интерфейсной переменной несколькими потоками НЕ является потокобезопасным. - person Rob Kennedy; 27.05.2009
comment
Однако это само по себе немного усложняет дело. Мне явно нужно вернуться в Delphi, чтобы проверить, что безопасно при использовании TInterfacedObject в многопоточном коде. - person jerryjvl; 28.05.2009

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

Кроме того, это решение не требует блокировок или мьютексов, но требует атомарной операции проверки и установки. Я не знаю Delphi и написал свое решение на Lisp, поэтому постараюсь описать его псевдокодом.

(CAPS - это имена функций, все эти функции не принимают и не возвращают аргументов)

integer access-mode = 1; // start in reader mode. 

WRITE  loop with current = accessmode, 
            with new = (current & 0xFFFFFFFe) 
            until test-and-set(access-mode, current to new)
       loop until access-mode = 0; 

ENDWRITE assert( access-mode = 0)
         set access-mode to 1

READ loop with current = ( accessmode | 1 ),
          with new = (current + 2),
          until test-and-set(access-mode, current to new)
ENDREAD loop with current = accessmode
             with new = (current - 2),
             until test-and-set(access-mode, current to new)

Для использования считыватель вызывает READ перед чтением и ENDREAD по завершении. Одинокий писатель вызывает WRITE перед записью и ENDWRITE по завершении.

Идея состоит в том, что целое число, называемое режимом доступа, содержит логическое значение в младшем бите и счетчик в старших битах. WRITE устанавливает бит в 0, а затем вращается до тех пор, пока ENDREAD не наберет достаточного количества обратного отсчета режима доступа до нуля. Endwrite устанавливает для режима доступа значение 1. READ ИЛИ текущее значение access-mode с 1, так что их проверка и установка пройдут только в том случае, если младший бит изначально был высоким. Я складываю и вычитаю на 2, чтобы оставить младший бит в покое.

Чтобы подсчитать количество читателей, просто сместите режим доступа вправо на единицу.

person Warren Wilkinson    schedule 20.05.2010