Каким будет код критической секции для общей очереди, к которой обращаются два потока?

Предположим, у нас есть общая очередь (реализованная с помощью массива), к которой могут обращаться два потока, один для чтения из нее данных, а другой для записи в нее данных. Теперь у меня проблема с синхронизацией. Я реализую это с помощью Win32 API (EnterCriticalSection и т. д.).

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

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

Какие потенциальные проблемы могут возникнуть?


person Rajendra Uppal    schedule 27.07.2011    source источник
comment
Что, если у вас есть только 1 запись, поэтому хвост = голова?   -  person Jesus Ramos    schedule 27.07.2011
comment
Все должно быть в порядке. Если у вас заблокирован раздел, модуль чтения получит данные без добавления данных модулем записи поверх модуля чтения, и модуль чтения не удалит данные, в результате чего смещение следующего пустого слота будет неверным для модуля записи. На самом деле, пока писатель никогда не меняет фасад, вам не нужна критическая секция вокруг него, за исключением той части, где читатель удаляет то, что он уже прочитал, если писатель одновременно пытается писать, где читатель меняет текст. следующее смещение вышло из-под писателя, потому что оно не было синхронизировано.   -  person Brain2000    schedule 27.07.2011


Ответы (1)


Для реализации циклической очереди с одним производителем/потребителем блокировки фактически не требуются. Просто установите условие, при котором производитель не может записывать в очередь, если очередь заполнена, и потребитель не может читать из очереди, если она пуста. Также производитель всегда будет записывать в указатель tail, указывающий на первый доступный пустой слот в очереди, а потребитель будет читать из указателя head, представляющего первый непрочитанный слот в очереди.

Ваш код может выглядеть как следующий пример кода (примечание: я предполагаю, что в инициализированной очереди tail == head и что оба указателя объявлены volatile, чтобы оптимизирующий компилятор не переупорядочивал последовательность операций в данном потоке. Вкл. x86 барьеры памяти не требуются из-за строгой модели согласованности памяти для архитектуры, но это изменится на других архитектурах с более слабыми моделями согласованности памяти, где потребуются барьеры памяти):

queue_type::pointer queue_type::next_slot(queue_type::pointer ptr);

bool queue_type::enqueue(const my_type& obj)
{
    if (next_slot(tail) == head)
        return false;

    *tail = obj;
    tail = next_slot(tail);

    return true;
}

bool queue_type::dequeue(my_type& obj)
{
    if (head == tail)
        return false;

    obj = *head;
    head = next_slot(head);

    return true;
}

Функция next_slot просто увеличивает указатель head или tail так, чтобы он возвращал указатель на следующий слот в массиве и учитывал любую функциональность переноса массива.

Наконец, мы гарантируем синхронизацию в модели с одним производителем/потребителем, потому что мы не увеличиваем указатель tail до тех пор, пока он не запишет данные в слот, на который он указывал, и мы не увеличиваем указатель head, пока мы не прочитаем данные из памяти. слот, на который он указывал. Поэтому вызов dequeue не будет считаться действительным до тех пор, пока не будет сделан хотя бы один вызов enequeue, а указатель tail никогда не перезапишет указатель head из-за проверки в enqueue. Кроме того, только один поток увеличивает указатель tail, а один поток увеличивает указатель head, поэтому нет проблем с общим чтением или записью из или в один и тот же указатель, которые могли бы создать проблемы синхронизации, требующие блокировки или некоторого типа атомарного операция.

person Jason    schedule 27.07.2011
comment
Жаль, я мог бы поставить только +1! Хороший ответ. - person Rajendra Uppal; 27.07.2011
comment
Это круто, если я не хочу ждать, если очередь занята. Поэтому я должен написать while(queue.enqueue(obj) != 0);, чтобы убедиться, что obj возвращен в очередь, а это не так уж и круто. - person ali_bahoo; 27.07.2011
comment
@sad_man: вы можете заблокировать производителя/потребителя при постановке/удалении из очереди, если очередь заполнена/пуста, и использовать события для пробуждения соответствующего потока. - person ChrisWue; 27.07.2011
comment
@ChrisWue: я думаю, что использование событий добавляет к проблеме почти такую ​​же (может быть, даже большую) сложность. Также использование критической секции является более эффективным решением. - person ali_bahoo; 27.07.2011
comment
Если вы работаете в многопроцессорной среде, спин-блокировки, безусловно, будут самым быстрым решением. Спин-блокировка — это не что иное, как форма ожидания атомарной переменной. Это решение позволяет избежать более медленных блокировок, которые должны быть разрешены ядром, и только когда очередь пуста или заполнена, требуется какое-либо ожидание занятости. Например, если бы эта очередь использовалась в качестве очереди сообщений в быстром конвейере данных, где вам нужны самые быстрые операции постановки в очередь и удаления из очереди для уменьшения задержки, вы бы вряд ли когда-либо столкнулись с длительным ожиданием занятости... вы определенно никогда не захотите прикоснуться к ядру. - person Jason; 27.07.2011
comment
C++ имеет тип bool. Используй это. - person Puppy; 27.07.2011