Блокировка чтения/записи с использованием только критической секции вызывает взаимоблокировку

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

Вот мои функции блокировки/разблокировки чтения/записи:

#include <windows.h>

typedef struct _RW_LOCK 
{
    CRITICAL_SECTION readerCountLock;
    CRITICAL_SECTION writerLock;
    int readerCount;
} RW_LOCK, *PRW_LOCK;

void InitLock(PRW_LOCK rwlock)
{
    InitializeCriticalSection(&rwlock->readerCountLock);
    InitializeCriticalSection(&rwlock->writerLock);
}

void ReadLock(PRW_LOCK rwlock)
{
    EnterCriticalSection(&rwlock->readerCountLock); // In deadlock 1 thread waits here (see description below)
    if (++rwlock->readerCount == 1) 
    {
        EnterCriticalSection(&rwlock->writerLock); // In deadlock 1 thread waits here
    }
    LeaveCriticalSection(&rwlock->readerCountLock);
}

void ReadUnlock(PRW_LOCK rwlock)
{
    EnterCriticalSection(&rwlock->readerCountLock);
    if (--rwlock->readerCount == 0) 
    {
        LeaveCriticalSection(&rwlock->writerLock);
    }
    LeaveCriticalSection(&rwlock->readerCountLock);
}

int WriteLock(PRW_LOCK rwlock)
{
    EnterCriticalSection(&rwlock->writerLock); // In deadlock 3 threads wait here
}

void WriteUnlock(PRW_LOCK rwlock)
{
    LeaveCriticalSection(&rwlock->writerLock);
}

А вот функция потока. После вызова InitLock (&g_rwLock); из main я создал ПЯТЬ потоков, чтобы попробовать эти блокировки.

void thread_function()
{
    static int value = 0;
    RW_LOCK g_rwLock;
    
    while(1)
    {
        ReadLock(&g_rwlLock);
        BOOL bIsValueOdd = value % 2;
        ReadUnlock(&g_rwlLock);

        WriteLock(&g_rwlLock);
        value ++;
        WriteUnlock(&g_rwlLock);
    }
}

В идеале этот код должен работать вечно без каких-либо проблем. Но, к моему разочарованию, он не работает всегда. Иногда это заходит в тупик. Я скомпилировал это и запустил на Windows XP. Для создания потоков с использованием пула потоков я использую стороннюю библиотеку. Следовательно, здесь нельзя привести весь этот код, который включает в себя множество процедур инициализации и прочего.

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

В приведенном выше коде я прокомментировал, что каждый поток (из ПЯТИ потоков) ожидает возникновения взаимоблокировки. (Я обнаружил это, подключив отладчик к заблокированному процессу)

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


person Atul    schedule 01.12.2014    source источник
comment
Обратите внимание, что это блокировка, предпочтительная для чтения — если количество ваших читателей никогда не достигает 0, запись будет ждать вечно. Если это нежелательно, вы можете подумать о системе продажи билетов, как в Меллор-Крамми-Скотт честный замок.   -  person Cory Nelson    schedule 01.12.2014
comment
Как обычно: _RW_LOCK — это имя, зарезервированное для компилятора (начальное подчеркивание, за которым следует заглавная буква)   -  person    schedule 01.12.2014
comment
Опубликуйте фактический код, демонстрирующий проблему. Код здесь явно что-то потерял в переводе и тратит время любезных людей.   -  person Cory Nelson    schedule 02.12.2014
comment
InitLock звонили из main. Я понял, что при публикации произошли ошибки. Я действительно в стрессе :( Позвольте мне попытаться подготовить код, который не будет использовать какую-либо стороннюю библиотеку, и опубликовать его здесь.   -  person Atul    schedule 02.12.2014
comment
Теперь, когда загадка решена (аля ответ Стива), обратите внимание, что если вам нужна производительность, использование взаимосвязанных операций для увеличения и уменьшения readerCount будет значительно быстрее, чем критический раздел.   -  person Harry Johnston    schedule 02.12.2014
comment
Отлично :) попробую.   -  person Atul    schedule 02.12.2014


Ответы (2)


Пока заметил две вещи:

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

Последнее соответствует тупику, который вы видите.

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

person Steve Jessop    schedule 01.12.2014
comment
Обратите внимание, что каждый поток имеет свою критическую секцию. Очевидно, это не то, что он имел в виду, но это означает, что LeaveCriticalSection в отдельном потоке не учитывает взаимоблокировку. - person Cory Nelson; 01.12.2014
comment
@Cory: о да, хорошо замечено. В этом случае ничего не должно блокироваться, и на value возникает состояние гонки. UB по стандарту, но не будет этих симптомов в Windows. - person Steve Jessop; 01.12.2014
comment
На самом деле, если подумать, что такое Value? Не то же самое, что value. Так что теперь я не знаю, чему мы можем доверять в показанном коде, а чему нет. Если на самом деле в реальном коде есть только одна критическая секция, возможно, она инициируется только один раз, но мой второй пункт все же может быть применим. - person Steve Jessop; 02.12.2014
comment
InitLock был вызван на самом деле из main. Но я подозреваю, что ваш второй пункт применим здесь. После того, как поток вызывает ReadLock и ReadUnlock, если другой поток опережает его, вызывая WriteLock и WriteUnlock, он по существу вызывает LeaveCriticalSection для объекта критической секции, в который он никогда не входил. - person Atul; 02.12.2014

Таким образом, он не может работать правильно.

  • позволяет 1 потоку войти в ReadLock(), позволить ему пройти инструкцию ++, но приостановить его перед входом в Writer CS
  • другой поток входит в WriteLock() и успешно входит в writeCS

так что теперь у нас есть количество читателей = 1 и работающий писатель одновременно. обратите внимание, что считыватель заблокирован на EnterCriticalSection(&rwlock->writerLock)

person Anonymous    schedule 01.12.2014
comment
Это означает, что читатель заблокирован до тех пор, пока писатель не покинет критическую секцию. Но что блокирует писателя в тупике, наблюдаемом вопрошающим? - person Steve Jessop; 01.12.2014
comment
Вероятно, вы правы, я неверно истолковал счетчики. - person Anonymous; 01.12.2014