Как стать автором
Обновить

Комментарии 18

Почему не использовать единую критическую секцию для работы со всеми внутренними данными?
Потому что в функции writeLock() есть такой код:
if (readerCount > 0)
{
    waitingWriter = TRUE;
    LeaveCriticalSection(&countsLock);
    WaitForSingleObject(noReaders, INFINITE);
}


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

и тут же другой писатель может перехватить контроль. В итоге первый писатель может остаться ждать в тот момент, когда читателей и писателей реально нет

Почему, если у нас выставлен флаг waitingWriter? Второй писатель не должен получить доступ, раз уже есть один ожидающий — он должен сам свалиться в Wait, а тем временем будет разбужен первый писатель.
Так при отпуске секции надо «будить» всех ждущих.

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

Почему, если у нас выставлен флаг waitingWriter? Второй писатель не должен получить доступ, раз уже есть один ожидающий — он должен сам свалиться в Wait, а тем временем будет разбужен первый писатель.


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

С критической секцией rwlock делается либо с двумя, либо с одной + условная переменная.
Тем не менее, эта задача решается с одной блокировкой.

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

Это не должно влиять на ожидание писателем, он будет по-прежнему в очереди. При выполнении операции «отпуск» мы видим, что у нас есть ожидающий писатель, и можем разбудить его. Или же читателя в противном случае.
Если после отпускания секции и перед началом ожидания события сперва встрянет читатель, то он скинет этот флаг при разблокировке

Нужен счетчик, а не флаг в таком случае.
Тем не менее, эта задача решается с одной блокировкой.

Может быть, тогда вы представите это решение?
Это не должно влиять на ожидание писателем, он будет по-прежнему в очереди. При выполнении операции «отпуск» мы видим, что у нас есть ожидающий писатель, и можем разбудить его. Или же читателя в противном случае.

Вы совсем не слышите, что вам говорят. В момент перехвата читателем и в момент его отпуска нет никаких ожидающих писателей. Либо есть ожидающий писатель, но не исходный, а другой — тоже перехвативший управление.
Нужен счетчик, а не флаг в таком случае.

Либо вы что-то не понимаете в многопоточности, либо мои понятия о ней устарели. Счетчик вас не спасет, потому что читатель может перехватить управление несколько раз подряд, успешно обнулив счетчик.

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

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

Вы совсем не слышите, что вам говорят. В момент перехвата читателем и в момент его отпуска нет никаких ожидающих писателей. Либо есть ожидающий писатель, но не исходный, а другой — тоже перехвативший управление.

В каждый момент времени у вас все данные защищены критической секцией. Не может никто внезапно «тоже» перехватить управление. Если у вас другой поток просит дать ему право читать/писать, блокировка имеет право ему отказать на время. Будится за раз только один ожидающий записи спящий поток, пусть их хоть десяток, на чтение — все.

Либо вы что-то не понимаете в многопоточности, либо мои понятия о ней устарели. Счетчик вас не спасет, потому что читатель может перехватить управление несколько раз подряд, успешно обнулив счетчик.

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

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

Да нет, просто вы решаете задачу несколько иначе.

Я не могу полностью дать вам код, но часть — BeginWrite с купюрами выглядит так:

  MustWait := False;
  EnterCriticalSection(Lock);
  try
    ThreadId := GetCurrentThreadId;
    Index := FindThread(ThreadId);
    if Index < 0 then
    begin
      // Request to write (first time)
      AddToThreadList(ThreadId, False);
      if State = 0 then
      begin
        // unowned so start writing
        State := -1;
      end
      else
      begin
        // owned, must wait
        Inc(WaitingWriters);
        MustWait := True;
      end;
    end
    else
    begin
    ...
    ...
    end;
  finally
    LeaveCriticalSection(Lock);
  end;
  if MustWait then
    WaitForSingleObjectEx(SemWriters, INFINITE, True);
....
....
procedure Release
        if State = 0 then
          ReleaseWaiters(WasReading);

procedure ReleaseWaiters
... если решили дать зеленый свет писателям:
        State := -1;
        Dec(WaitingWriters);
        ReleaseSemaphore(SemWriters, 1, nil);



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

Во-первых, реализация под Vista не является reentrant, что прямо следует из документации:
An SRW lock is the size of a pointer. The advantage is that it is fast to update the lock state. The disadvantage is that very little state information can be stored, so SRW locks cannot be acquired recursively.

В то время как критическая секция является reentrant, откуда вы получаете разное поведение под разные версии ОС.

Во-вторых, зачем городить огород из двух критических секций и события, когда можно обойтись одним событием и атомарными операциями? Это лишние накладные расходы. Обратите внимание на название примитива под Vista — это не просто так, что он занимает в памяти всего размер указателя. Для примера, как можно обойтись без критических секций, я делал в свое время это на C так (тоже под разные версии ОС). В свою очередь, есть уже готовая библиотека с выбором реализаций на любой вкус: RWLock (neosmart).

То есть представленное решение далеко не самое оптимальное, я бы не рекомендовал его использовать.
Строго говоря, да, под XP у нас reentrant, а под все остальное нет.
Но я исходил из того, что пишем мы не на XP. Для меня лично, основные платформы это: Windows 7/8/10 + Linux и поведение на них будет идентичным. То что на XP будет реентерабельность, проблемы в таком случае не составит (код то мы писали и отлаживали исходя из ее отсутствия).

Строго говоря, да, под XP у нас reentrant, а под все остальное нет.

Я бы так не сказал точно, но в большинстве случаев, это будет скорее верно. Нюанс в том, что стандарт POSIX не накладывает ограничения на это:
Results are undefined if the calling thread holds the read-write lock (whether a read or write lock) at the time the call is made.

То есть лучше на это не рассчитывать, так как Linux имеет полное право использовать рекурсивные блокировки.

Windows 7/8/10

Честно говоря, не понимаю, почему вы так выкидываете из обоймы XP, когда ее рыночная доля сопоставима с 8-й. Конечно, проще выкинуть и не заморачиваться, но ведь куда интереснее сделать реально кроссплатформенную вещь :) Без XP ваша статья как бы теряет смысл.
Я не выкидываю XP, именно из-за необходимости ее поддержки эта статья и появилась.
Но разрабатываю я не на на XP по вполне понятным причинам. Соответственно, никаких проблем с реентерабельностью возникнуть не должно, так как мы изначально рассчитываем что ее нет и она нам не нужна. Если же она нам зачем-то нужна, то вариантов использовать Slim API как-бы и нет, только тот страшный малопонятный огород по вашим ссылкам, только хардкор да.
У меня, в свое время, тоже была мысль сделать блокировщик, позволяющий делать SingleWrite и MultipleRead, и насколько я был рад тому что это уже сделано, настолько и разочарован, что увы, только в Висте. Подобную реализацию под XP видел, но мне в ней не понравилась одна вещь: если в потоке блокировщик уже открыт на чтение, то при открытии в нем же на запись будет дедлок. Вечное ожидание того, что количества ридеров будет равно нулю, т.к. один из ридеров и есть этот самый поток. Отчасти соглашусь, что при открытом на чтение объекте, открывать его на запись в том же потоке архитектурно неверно. Но тем не менее не хотелось бы попадать в дедлок в этой ситуации и я сделал похожую реализацию с использованием TlsSetValue и TlsGetValue чтобы записать в поток кол-ко его собственных ридеров для блокировщика и если оставшееся число открытых ридеров ему равно — то можно открывать на запись или выпадать в ассерт (мол, ты что творишь, ирод!)

Чем изобретать велосипед, для WinXP лучше пользоваться недокументированными функциями: RtlAcquireResourceShared() (лок на чтение) и RtlAcquireResourceExclusive (лок на запись).


Пример на Delphi, как это можно обернуть в RWLock интерфейс: u_ReadWriteSyncRtlResource.pas.


Тот же StackOverflow рекомендует: Is there a cross platform version of window vista's slim reader writer locks?

Спасибо, добавил их в статью. Эти функции действительно хороши.
А не проще для Win XP сделать workaround в виде RLock?

Чем плохи std::shared_mutex и boost::shared_mutex ???

Всегда интереснее, как оно устроено внутри на пользовательском уровне.


А статья — хороший повод залезть в код и заменить самописные костыли на API, покуда в нынешнее время уже нет смысла писать код под windows XP, а от его legacy очень хочется избавиться наконец.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации