RWLock — это такой примитив синхронизации, позволяющий одновременное чтение и эксклюзивную запись. Т.е. чтение блокирует запись, но не блокирует чтение других тредов, а запись блокирует все.

Так вот, этот весьма полезный примитив имеется в posix threads и в Windows от Vista и далее. Для Windows XP/2003 приходится изготавливать сей, весьма полезный, примитив из двух критических секций и события.

Покажем как это выглядит (код любезно предоставлен StackOverflow и слегка переведен с C на C++):

class RWLockXP // Implementation for Windows XP
{
public:
    RWLockXP()
        : countsLock(),
          writerLock(),
          noReaders(),
          readerCount(0),
          waitingWriter(FALSE)
    {
        InitializeCriticalSection(&writerLock);
        InitializeCriticalSection(&countsLock);

       /*
        * Could use a semaphore as well.  There can only be one waiter ever,
        * so I'm showing an auto-reset event here.
        */
        noReaders = CreateEvent (NULL, FALSE, FALSE, NULL);
    }

    ~RWLockXP()
    {
        writerLock.destroy();
        readerCountLock.destroy();
        noReaders.destroy();
    }

    void readLock()
    {
        /**
         * We need to lock the writerLock too, otherwise a writer could
         * do the whole of rwlock_wrlock after the readerCount changed
         * from 0 to 1, but before the event was reset.
         */

        EnterCriticalSection(&writerLock);
        EnterCriticalSection(&countsLock);
        ++readerCount;
        LeaveCriticalSection(&countsLock);
        LeaveCriticalSection(&writerLock);
    }
    void readUnLock()
    {
        EnterCriticalSection(&countsLock);
        assert (readerCount > 0);
        if (--readerCount == 0)
        {
            if (waitingWriter)
            {
                /*
                 * Clear waitingWriter here to avoid taking countsLock
                 * again in wrlock.
                 */
                waitingWriter = FALSE;
                SetEvent(noReaders);
            }
        }
        LeaveCriticalSection(&countsLock);
    }

    void writeLock()
    {
        EnterCriticalSection(&writerLock);
        /*
         * readerCount cannot become non-zero within the writerLock CS,
         * but it can become zero...
         */
        if (readerCount > 0)
        {
            EnterCriticalSection(&countsLock);

            /* ... so test it again.  */
            if (readerCount > 0)
            {
                waitingWriter = TRUE;
                LeaveCriticalSection(&countsLock);
                WaitForSingleObject(noReaders, INFINITE);
            }
            else
            {
                /* How lucky, no need to wait.  */
                LeaveCriticalSection(&countsLock);
            }
        }
        /* writerLock remains locked.  */
    }

    void writeUnLock()
    {
        LeaveCriticalSection(&writerLock);
    }

private:
    CRITICAL_SECTION countsLock;
    CRITICAL_SECTION writerLock;
    HANDLE noReaders;
    int readerCount;
    BOOL waitingWriter;
};

А вот как эта же красота могла бы выглядеть при использовании только Vista+ систем:

class RWLockSRW // For Windows Vista+ based on Slim RWLock
{
public:
    RWLockSRW()
        : srwLock()
    {
        InitializeSRWLock(&srwLock);
    }

    ~RWLockSRW()
    {
    }

    void readLock()
    {
        AcquireSRWLockShared(&srwLock);
    }

    void readUnLock()
    {
        ReleaseSRWLockShared(&srwLock);
    }

    void writeLock()
    {
        AcquireSRWLockExclusive(&srwLock);
    }

    void writeUnLock()
    {
        ReleaseSRWLockExclusive(&srwLock);
    }

private:
    RTL_SRWLOCK srwLock;
};

Мало того, что выглядит до безобразия просто, еще и работает на порядок быстрее. Но, есть одно но, как всегда… При попытке запустить приложение содержащее этот код (конечно мы умные ребята, сделали определение версии и для XP хотим использовать первый вариант, а для новых систем — второй), получим сообщение типа: «ой, а вот функции InitializeSRWLock что-то не нашлось в kernel32» после чего наше приложение любезно будет прибито.

Выход — грузить функции Slim RWLock динамически при помощи LoadLibrary, указателей на функции и этого всего:

typedef void(__stdcall *SRWLock_fptr)(PSRWLOCK);

class RWLockSRW // For Windows Vista+ based on Slim RWLock
{
public:
    RWLockSRW()
        : hGetProcIDDLL(NULL), 
        AcquireSRWLockShared_func(NULL),
        ReleaseSRWLockShared_func(NULL),
        AcquireSRWLockExclusive_func(NULL),
        ReleaseSRWLockExclusive_func(NULL),
        srwLock()
    {
        wchar_t path[MAX_PATH] = { 0 };
        GetSystemDirectory(path, sizeof(path));
        std::wstring dllPath = std::wstring(path) + L"\\kernel32.dll";
        HINSTANCE hGetProcIDDLL = LoadLibrary(dllPath.c_str());
        if (!hGetProcIDDLL)
        {
            throw std::exception("SRWLock Error loading kernel32.dll");
        }

        AcquireSRWLockShared_func = (SRWLock_fptr)GetProcAddress(hGetProcIDDLL, "AcquireSRWLockShared");
        if (!AcquireSRWLockShared_func)
        {
            throw std::exception("SRWLock Error loading AcquireSRWLockShared");
        }

        ReleaseSRWLockShared_func = (SRWLock_fptr)GetProcAddress(hGetProcIDDLL, "ReleaseSRWLockShared");
        if (!ReleaseSRWLockShared_func)
        {
            throw std::exception("SRWLock Error loading ReleaseSRWLockShared");
        }

        AcquireSRWLockExclusive_func = (SRWLock_fptr)GetProcAddress(hGetProcIDDLL, "AcquireSRWLockExclusive");
        if (!AcquireSRWLockExclusive_func)
        {
            throw std::exception("SRWLock Error loading AcquireSRWLockExclusive");
        }

        ReleaseSRWLockExclusive_func = (SRWLock_fptr)GetProcAddress(hGetProcIDDLL, "ReleaseSRWLockExclusive");
        if (!ReleaseSRWLockExclusive_func)
        {
            throw std::exception("SRWLock Error loading ReleaseSRWLockExclusive");
        }

        SRWLock_fptr InitializeSRWLock_func = (SRWLock_fptr)GetProcAddress(hGetProcIDDLL, "InitializeSRWLock");
        if (!InitializeSRWLock_func)
        {
            throw std::exception("SRWLock Error loading InitializeSRWLock");
        }

        InitializeSRWLock_func(&srwLock);
    }

    ~RWLockSRW()
    {
        if (hGetProcIDDLL)
        {
            FreeLibrary(hGetProcIDDLL);
        }
    }

    void readLock()
    {
        if (AcquireSRWLockShared_func)
        {
            AcquireSRWLockShared_func(&srwLock);
        }
    }

    void readUnLock()
    {
        if (ReleaseSRWLockShared_func)
        {
            ReleaseSRWLockShared_func(&srwLock);
        }
    }

    void writeLock()
    {
        if (AcquireSRWLockExclusive_func)
        {
            AcquireSRWLockExclusive_func(&srwLock);
        }
    }

    void writeUnLock()
    {
        if (ReleaseSRWLockExclusive_func)
        {
            ReleaseSRWLockExclusive_func(&srwLock);
        }
    }

private:
    HINSTANCE hGetProcIDDLL;

    SRWLock_fptr AcquireSRWLockShared_func;
    SRWLock_fptr ReleaseSRWLockShared_func;
    SRWLock_fptr AcquireSRWLockExclusive_func;
    SRWLock_fptr ReleaseSRWLockExclusive_func;

    RTL_SRWLOCK srwLock;
};

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

class RWLock // Wrapper
{
public:
     RWLock()
         : rwLockXP(NULL), rwLockSRW(NULL), isVistaPlus(IsWindowsVistaOrGreater())
     {
          if (isVistaPlus)
          {
               rwLockSRW = new RWLockSRW();
          }
          else
          {
               rwLockXP = new RWLockXP();
          }
     }

     ~RWLock()
     {
          if (isVistaPlus)
          {
               delete rwLockSRW;
          }
          else
          {
               delete rwLockXP;
          }
     }
     void readLock()
     {
          if (isVistaPlus)
          {
               rwLockSRW->readLock();
          }
          else
          {
               rwLockXP->readLock();
          }
     }
     void readUnLock()
     {
          if (isVistaPlus)
          {
               rwLockSRW->readUnLock();
          }
          else
          {
               rwLockXP->readUnLock();
          }
     }

     void writeLock()
     {
          if (isVistaPlus)
          {
               rwLockSRW->writeLock();
          }
          else
          {
               rwLockXP->writeLock();
          }
     }
     void writeUnLock()
     {
          if (isVistaPlus)
          {
               rwLockSRW->writeUnLock();
          }
          else
          {
               rwLockXP->writeUnLock();
          }
     }

private:
     RWLockXP  *rwLockXP;
     RWLockSRW *rwLockSRW;
     bool isVistaPlus;
};

И в завершении автолокер:

class ScopedRWLock
{
public:
    ScopedRWLock(RWLock *lc_, bool write_ = false)
        : lc(*lc_), write(write_)
    {
        if (write)
        {
            lc.writeLock();
        }
        else
        {
            lc.readLock();
        }
    }

    ~ScopedRWLock()
    {
        if (write)
        {
            lc.writeUnLock();
        }
        else
        {
            lc.readUnLock();
        }
    }
private:
    RWLock &lc;
    bool write;

    // Non copyable!
    static void *operator new(size_t);
    static void operator delete(void *);
    ScopedRWLock(const ScopedRWLock&);
    void operator=(const ScopedRWLock&);
};

Реализация с использованием pthread ничем не отличается от первой версии SRWLock за исключением других имен вызываемых функций.
Поделиться с друзьями
-->

Комментарии (17)


  1. MacIn
    19.12.2016 19:51
    +1

    Почему не использовать единую критическую секцию для работы со всеми внутренними данными?


    1. passanger2012
      19.12.2016 21:35

      Потому что в функции writeLock() есть такой код:

      if (readerCount > 0)
      {
          waitingWriter = TRUE;
          LeaveCriticalSection(&countsLock);
          WaitForSingleObject(noReaders, INFINITE);
      }
      


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


      1. MacIn
        19.12.2016 22:17

        Так при отпуске секции надо «будить» всех ждущих.

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

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


        1. passanger2012
          20.12.2016 00:31

          Так при отпуске секции надо «будить» всех ждущих.

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

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


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

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


          1. MacIn
            20.12.2016 03:09

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

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

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

            Нужен счетчик, а не флаг в таком случае.


            1. passanger2012
              20.12.2016 12:58

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

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

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

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

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


              1. MacIn
                22.12.2016 03:36

                Может быть, тогда вы представите это решение?

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

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

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

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

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

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

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

                Я не могу полностью дать вам код, но часть — 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 указывает, захвачен ли ресурс на чтение (и скольки потоками сразу), запись, или свободен. Учитывается кол-во ожидающих писателей, за раз отпускается только один пишущий поток.


                1. passanger2012
                  22.12.2016 18:18

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


  1. passanger2012
    19.12.2016 21:13
    +4

    Вставлю свои пять копеек.

    Во-первых, реализация под 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).

    То есть представленное решение далеко не самое оптимальное, я бы не рекомендовал его использовать.


    1. udattsk
      20.12.2016 06:21

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


      1. passanger2012
        20.12.2016 16:31

        Строго говоря, да, под 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 ваша статья как бы теряет смысл.


        1. udattsk
          20.12.2016 17:02

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


  1. CodeName33
    20.12.2016 06:05

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


  1. zedxxx
    20.12.2016 06:22

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


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


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


  1. malbaron
    20.12.2016 06:22

    А не проще для Win XP сделать workaround в виде RLock?


  1. ww898
    20.12.2016 06:22

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


    1. klirichek
      20.12.2016 09:07

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


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