Каким будет код критической секции для общей очереди, к которой обращаются два потока? - PullRequest
8 голосов
/ 27 июля 2011

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

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

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

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

1 Ответ

6 голосов
/ 27 июля 2011

Для реализации циклической очереди с одним производителем / потребителем блокировки фактически не требуются.Просто установите условие, при котором производитель не может записывать в очередь, если очередь заполнена, а потребитель не может читать из очереди, если она пуста.Также производитель всегда будет писать в указатель 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, поэтому не возникает проблем с общим чтением или записью из одного и того же указателя, которые могут создать проблемы синхронизации, требующие блокировки илинекоторый тип атомарной операции.

...