Многопоточная программа зависла в оптимизированном режиме, но нормально работает в -O0

Я написал простую многопоточную программу следующим образом:

static bool finished = false;

int func()
{
    size_t i = 0;
    while (!finished)
        ++i;
    return i;
}

int main()
{
    auto result=std::async(std::launch::async, func);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    finished=true;
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

Он ведет себя нормально в режиме отладки в Visual Studio или -O0 в gc c и распечатайте результат через 1 секунд. Но он застрял и ничего не печатает в режиме Release или -O1 -O2 -O3.

67
задан curiousguy 24 October 2019 в 04:46
поделиться

3 ответа

Два потока, получая доступ к неатомарной, неохраняемой переменной U.B. Это касается finished. Вы могли сделать finished из типа std::atomic<bool> для фиксации этого.

Моя фиксация:

#include <iostream>
#include <future>
#include <atomic>

static std::atomic<bool> finished = false;

int func()
{
    size_t i = 0;
    while (!finished)
        ++i;
    return i;
}

int main()
{
    auto result=std::async(std::launch::async, func);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    finished=true;
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

Вывод:

result =1023045342
main thread id=140147660588864

Живая Демонстрация на <часе> coliru

Кто-то может думать, что 'Это bool – вероятно, один бит. Как это может быть неатомарным?' (Я сделал, когда я запустил с многопоточности сам.)

, Но примечание, что отсутствие разрыва не является единственной вещью, которая std::atomic дает Вам. Это также делает параллельный read+write доступ из нескольких потоков четко определенным, мешая компилятору предположить, что перечитывание переменной будет всегда видеть то же значение.

Создание bool неосторожный, неатомарный может вызвать дополнительные проблемы:

  • компилятор мог бы решить оптимизировать переменную в регистр или даже множественные доступы CSE в один и поднять загрузку из цикла.
  • переменная могла бы кэшироваться для ядра процессора. (В реальной жизни центральные процессоры имеют когерентные кэши . Это не настоящая проблема, но стандарт C++ достаточно свободен для покрытия гипотетических реализаций C++ на некогерентной общей памяти, где atomic<bool> с memory_order_relaxed хранилище/загрузка работало бы, но где volatile не будет. Используя энергозависимый для этого был бы UB, даже при том, что он работает на практике над реальными реализациями C++.)

Для предотвращения этого для случая компилятор должен быть сказан явно не сделать.

<час>

я немного удивлен о развивающемся обсуждении относительно потенциального отношения [1 111] к этой проблеме. Таким образом я хотел бы к потраченному свои два цента:

100
ответ дан 24 November 2019 в 14:33
поделиться

Ответ Scheff описывает, как исправить Ваш код. Я думал, что включу немного информации, что на самом деле происходит в этом случае.

я скомпилировал Ваш код в [1 115] godbolt с помощью уровня 1 (-O1) оптимизации. Ваша функция компилирует как так:

func():
  cmp BYTE PTR finished[rip], 0
  jne .L4
.L5:
  jmp .L5
.L4:
  mov eax, 0
  ret

Так, что происходит здесь? Во-первых, у нас есть сравнение: cmp BYTE PTR finished[rip], 0 - это проверяет, чтобы видеть, ли finished ложь или нет.

, Если это не ложь (иначе верный) мы должны выйти из цикла на первом показе. Выполненный jne .L4, который j судьи, когда квалификация n ot e для маркировки .L4, где значение i (0) хранится в регистре для более позднего использования и функциональных возвратов.

, Если это ложь однако, мы перемещаемся в [1 123]

.L5:
  jmp .L5

, Это - безусловный переход, для маркировки .L5, который именно так, оказывается, сама команда перехода.

, Другими словами, поток помещается в бесконечный занятый цикл.

Итак, почему это произошло?

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

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

оптимизированный код обеспечивает два пути выполнения кода, которые будут следовать из ввода функции с постоянным значением bool; или это выполняет цикл бесконечно, или цикл никогда не выполняется.

в [1 113] компилятор (как ожидалось) не оптимизирует тело цикла и сравнение далеко:

func():
  push rbp
  mov rbp, rsp
  mov QWORD PTR [rbp-8], 0
.L148:
  movzx eax, BYTE PTR finished[rip]
  test al, al
  jne .L147
  add QWORD PTR [rbp-8], 1
  jmp .L148
.L147:
  mov rax, QWORD PTR [rbp-8]
  pop rbp
  ret

поэтому функция при неоптимизации действительно работает, отсутствие атомарности здесь обычно является не проблемой, потому что код и тип данных просты. Вероятно, худшим, с которым мы могли столкнуться здесь, является значение [1 114], который выключен одним к тому, чем он должен быть.

А более сложная система со структурами данных, намного более вероятно, приведет к поврежденным данным или неподходящему выполнению.

42
ответ дан 24 November 2019 в 14:33
поделиться

Ради полноты в кривой обучения; необходимо избегать использования глобальных переменных. Вы сделали хорошее задание, хотя путем создания этого статичным, таким образом, это будет локально для единицы перевода.

Вот пример:

class ST {
public:
    int func()
    {
        size_t i = 0;
        while (!finished)
            ++i;
        return i;
    }
    void setFinished(bool val)
    {
        finished = val;
    }
private:
    std::atomic<bool> finished = false;
};

int main()
{
    ST st;
    auto result=std::async(std::launch::async, &ST::func, std::ref(st));
    std::this_thread::sleep_for(std::chrono::seconds(1));
    st.setFinished(true);
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

Живой на wandbox

5
ответ дан 24 November 2019 в 14:33
поделиться
Другие вопросы по тегам:

Похожие вопросы: