Многопоточная программа зависла в оптимизированном режиме, но нормально работает в -O0 - PullRequest
66 голосов
/ 23 октября 2019

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

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.

Ответы [ 3 ]

101 голосов
/ 23 октября 2019

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

Создание bool неохраняемой неатомной банкивызывают дополнительные проблемы:

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

Чтобы этого не случилось,Компилятору нужно явно запретить.


Я немного удивлен развивающейся дискуссией о потенциальном отношении volatile к этой проблеме. Таким образом, я хотел бы потратить два моих цента:

42 голосов
/ 23 октября 2019

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

Я скомпилировал ваш код на 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 ложным или нет.

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

Если оно равно ложь, однако, мы переходим к

.L5:
  jmp .L5

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

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

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

Что касается оптимизатора, потоки находятся за пределами его компетенции. Предполагается, что другие потоки не читают и не записывают переменные одновременно (потому что это будет гонка данных UB). Вы должны сказать ему, что он не может оптимизировать доступ. Именно здесь приходит ответ Шеффа. Я не буду его повторять.

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

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

при -O0 компилятор (как и ожидалось) не оптимизирует тело цикла и его сравнение:

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

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

Более сложная система со структурами данных гораздо более вероятнапривести к повреждению данных или неправильному выполнению.

5 голосов
/ 23 октября 2019

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

Вот пример:

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;
}

Live on wandbox

...