gcc атомные операции, вызывающие SEGV - PullRequest
1 голос
/ 28 сентября 2011

Я уже давно использую атомарную операцию gcc в многопоточном приложении, а вчера столкнулся с интересным сценарием, который я не могу объяснить.Эти элементарные функции перегружены и могут использовать типы данных шириной 1, 2, 4 или 8 байтов.В частности, я успешно использую операцию bool_compare_and_swap (CAS).Возникающая здесь проблема повторяется и возникает только тогда, когда я компилирую оптимизированный код (O3).Проблема не возникает, когда я компилирую неоптимизированную (O0).Имейте это в виду, так как я считаю, что оптимизатор делает что-то необычное в случае, который я здесь представляю.

Обычно я создаю объединение структуры, содержащей именованные типы (chars, shorts и т. Д.).) и тип данных соответствующего размера, который «вписывается» в эту структуру в один объект (т. е. long long). В этом случае у меня есть 8-байтовый (long long) в объединении, аналог структуры которого содержит битовые поля.Пример определения типа данных показан ниже.Предполагается, что битовые поля могут быть изменены с помощью операторов присваивания, и после того, как все присвоения выполнены, 8-байтовый тип данных будет таким же, как и CAS'd.В частности, я использую:

bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)

, завернутый в макрос следующим образом:

define THD_CAS(ptr, oldVal, newVal) __sync_bool_compare_and_swap(ptr, oldVal, newVal)

У меня есть структура, определенная следующим образом:

typedef union _TSynchro {
    struct {
        int          *pFirstSynchWork;
        unsigned short   idTransaction;
        unsigned short       fNewTrans  :1,
            fFileBad        :1,                 
            fOpComplete :1,                 
            fCancelWork :1,                     
            fPurged     :1,
            fStatRequired   :1;
    } Data;
    // above struct is overlayed by this struct so we can CAS all values with a single 64 bit cas
    long long           n64;
} TSynchro;

ИтакУ меня есть цикл параллелизма (while (1)) в коде, который захватывает «снимок» текущего значения данных и сохраняет его в «Old», устанавливает биты в новой копии («New») данных и затем пытается выполнить операцию CAS.Если CAS преуспевает, я - поток, который изменил данные, и я вырываюсь из цикла.Если произошел сбой CAS, какой-то другой поток изменил данные подо мной, и я повторил попытку, захватив еще один «снимок» текущих данных.

void NewSynchro(TSynchro *pSynchro)
{
    volatile TSynchro   New;
    volatile TSynchro Old;

    while (1) // concurrency loop
    {
        Old.n64 = pSynchro->n64;
        New.n64 = Old.n64;
        New.Data.fOpComplete = 1;
        New.Data.fStatRequired = 0;
        if (fFileBad)
        {
            New.Data.fFileBad = 1;
        }
        else
        {
            New.Data.fReleased = 0;
            New.Data.fFileBad = 0;
        }

        if (THD_CAS(&pSynchro->n64, Old.n64, New.n64))
            break;  // success
    }
}

Теперь вот что интересно ... Посмотрите, что яобъявляете Старое и Новое изменчивым?Хорошо, если ОБА Старый и Новый не имеют изменчивой модификации, я получаю SEGV, когда перехожу к следующему вызову функции после вызова NewSynchro ().Если EITH OLD или NEW или ОБА имеют модификатор volatile, код приложения никогда не будет SEGV.В разработке я сейчас запускаю только 1 поток (реальной угрозы раздора за изменение значения нет), поэтому я также попытался избавиться от CAS и заменить его простым присваиванием (т. Е. PSynchro-> n64 = New.n64), и приложение тоже работает нормально.

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

Мысли?

1 Ответ

3 голосов
/ 04 ноября 2011

Позвольте мне высказать несколько соображений: Союз обычно предназначен для использования либо: либо: либо long long, либо struct.В этом случае вы используете оба, и это работает как на большинстве простых процессоров.Однако, если у вас сложный конвейерный процессор, вы можете столкнуться с необходимостью для барьеров памяти или чего-то подобного.Более конкретно: установка бита в битовом поле является операцией чтения-изменения-записи.Проблема может возникнуть при выполнении этих операций в следующем порядке:

New.n64 = Old.n64;
New.Data.fOpComplete = 1;

Из-за объединения чтение для установки бита может быть начато до завершения записи n64.Этому конвейеру может противостоять компилятор, вставляющий сброс конвейера или барьеры памяти.Но используя объединение, компилятор может предполагать, что элементы являются отдельными: «Объединение может содержать только одно из его значений компонента за один раз».(H & S5, 5.7.1) Он не будет склонен вставлять какой-либо флеш / барьер, особенно при использовании агрессивной оптимизации, такой как -O3.И использование volatile для New также направит компилятор (даже с -O3), чтобы убедиться, что чтение / запись правильно разделены.Почему объявление Old volatile предотвращает ваши проблемы, я не могу сказать.Тогда мне нужно будет увидеть и сравнить сгенерированный код сборки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...