Реализация очереди без блокировки заканчивается циклом под нагрузкой

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

Приложение работает (и не работает) как в Linux, так и в Windows. Я выполняю отладку в Windows, где мой COMPARE_EXCHANGE_PTR сопоставляется с InterlockedCompareExchangePointer .

Это код, который помещает запрос в начало списка и вызывается из нескольких потоков:

void push_request(struct request * volatile * root, struct request * request)
{
    assert(request);

    do {
        request->next = *root;
    } while(COMPARE_EXCHANGE_PTR(root, request, request->next) != request->next);
}

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

struct request * pop_request(struct request * volatile * root)
{
    struct request * volatile * p;
    struct request * request;

    do {
        p = root;
        while(*p && (*p)->next) p = &(*p)->next; // <- loops here
        request = *p;
    } while(COMPARE_EXCHANGE_PTR(p, NULL, request) != request);

     assert(request->next == NULL);

     return request;
}

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

Есть несколько мест, которые помещают запрос в очередь, но все они выглядят примерно так:

// device->requests is defined as struct request * volatile requests;
struct request * request = malloc(sizeof(struct request));
if(request) {
    // fill out request fields
    push_request(&device->requests, request);
    sem_post(device->request_sem);
}

Код, обрабатывающий запрос, делает больше, но по сути делает это в цикле:

if(sem_wait_timeout(device->request_sem, timeout) == sem_success) {
    struct request * request = pop_request(&device->requests);
    // handle request
    free(request);
}

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

Когда я прерываю зависшую программу, поток обработчика зацикливается на pop_request в отмеченной позиции. У меня есть действительный список из одного или нескольких запросов, и последний следующий указатель указывает на себя. Очереди запросов обычно короткие, я никогда не видел больше 10, и только 1 и 3 два раза я мог посмотреть на этот сбой в отладчике.

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

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

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

Итак, вопрос в следующем: может ли кто-нибудь увидеть, как это можно сломать? Может ли кто-нибудь доказать, что это невозможно?

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


person Fozi    schedule 26.04.2010    source источник
comment
Этот цикл бесконечен при работе с одним потоком, который одновременно отправляет и выталкивает запросы? Один из имеющихся у вас инвариантов состоит в том, что пустой список представлен NULL, но не очевидно, что вы начинаете с пустого списка, установленного в NULL. Кроме того, если вы считаете, что отправляете повторяющиеся запросы, вы должны утверждать, что request->next==NULL в начале push_request().   -  person MSN    schedule 26.04.2010
comment
Да, я начинаю с device->requests, содержащего NULL. request->next перезаписывается в первой строке цикла do-while. Под дублирующим запросом я подразумеваю push_request(request); push_request(request);. В этом случае я бы получил петлю.   -  person Fozi    schedule 26.04.2010


Ответы (1)


Это тонко, но у вас есть состояние гонки.

Начните со списка с одним элементом в нем, req1. Итак, у нас есть:

device->requests == req1;
req1->next == NULL;

Теперь мы добавляем новый элемент req2 и одновременно пытаемся вытолкнуть очередь. Толчок для req2 начинается первым. Тело цикла while выполняется, так что теперь у нас есть:

device->requests == req1;
req1->next == NULL;
req2->next == req1;

Затем запускается COMPARE_EXCHANGE_PTR, так что у нас есть:

device->requests == req2;
req1->next == NULL;
req2->next == req1;

...и COMPARE_EXCHANGE_PTR() возвращает req1. Теперь, в этот момент, перед сравнением в условии while, отправка прерывается, и начинается выталкивание.

Выталкивание выполняется корректно до завершения, выталкивая req1 - это значит, что у нас есть:

device->requests == req2;
req2->next == NULL;

Пуш перезапускается. Теперь он извлекает request->next для сравнения, а также извлекает новое значение req2->next, которое равно NULL. Он сравнивает req1 с NULL, сравнение проходит успешно, цикл while выполняется снова, и теперь мы имеем:

device->requests == req2;
req2->next == req2;

На этот раз тест завершается неудачно, цикл while завершается, и вы получаете свой цикл.


Это должно исправить это:

void push_request(struct request * volatile * root, struct request * request)
{
    struct request *oldroot;

    assert(request);

    do {
        request->next = oldroot = *root;
    } while(COMPARE_EXCHANGE_PTR(root, request, oldroot) != oldroot);
}
person caf    schedule 27.04.2010
comment
@caf, Итак, прочитав это три раза и выключив музыку, чтобы я мог сосредоточиться, основная проблема заключается в том, что существует незащищенная запись в request->next внутри pop_request, которая нарушает предположение о том, что только push_request изменяет request->next. Правильно? - person MSN; 30.04.2010
comment
@MSN: Да, хотя запись защищена обменом атомарным сравнением - проблема в том, что она читается дважды в push_request, и только одно чтение защищено. - person caf; 30.04.2010