Логический стоп-сигнал между потоками - PullRequest
4 голосов
/ 10 июля 2020

Каков самый простой способ сигнализировать фоновому потоку о прекращении выполнения?

Я использовал что-то вроде:

volatile bool Global_Stop = false;

void do_stuff() {
    while (!Global_Stop) {
        //...
    }
}

Что-то не так? Я знаю, что для сложных случаев вам может понадобиться "atomi c" или мьютексы, но только для логической сигнализации это должно работать, верно?

Ответы [ 3 ]

5 голосов
/ 10 июля 2020

std::atomic не для «сложных случаев». Это когда вам нужно получить доступ к чему-то из нескольких потоков. Есть некоторые мифы о volatile, я не могу их вспомнить, потому что все, что я помню, это то, что volatile не помогает, когда вам нужно получить доступ к чему-то из разных потоков. Вам нужен std::atomic<bool>. Независимо от того, является ли на вашем реальном оборудовании доступ к bool atomi c, на самом деле не имеет значения, потому что для C ++ это не так.

3 голосов
/ 10 июля 2020

Да, есть проблема: это не гарантирует работу в C ++. Но это очень просто исправить, если вы используете хотя бы C ++ 11: используйте вместо этого std::atomic<bool>, например:

#include <atomic>

std::atomic<bool> Global_Stop = false;

void do_stuff() {
    while (!Global_Stop) {
        //...
    }
}

Одна проблема заключается в том, что компилятору разрешено переупорядочивать обращения к памяти, если он может доказать, что это не изменит эффект программы:

int foo() {
    int i = 1;
    int j = 2;
    ++i;
    ++j;
    return i + j;
}

Здесь компилятору разрешено увеличивать j перед i, потому что это явно не изменит эффект программы. Фактически, он может оптимизировать все это до return 5;. Итак, что считается «не изменит эффекта программы»? Ответ длинный и сложный, и я не претендую на то, чтобы понять их все, но одна его часть заключается в том, что компилятор должен беспокоиться только о потоках в определенных контекстах. Если бы i и j были глобальными переменными вместо локальных, он все равно мог бы поменять местами ++i и ++j, потому что разрешено предположить, что к ним обращается только один поток , если вы не используете определенные примитивы потока (например, mutex).

Теперь, когда дело доходит до такого кода:

while (!Global_Stop) {
    //...
}

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

if (!Global_Stop) {
    while (true) {
        //...
    }
}

Если он сможет доказать, что Global_Stop равен false в начале, он может даже удалить if проверьте!

На самом деле дела обстоят еще хуже, по крайней мере теоретически. Видите ли, если поток находится в процессе записи в переменную, когда другой поток обращается к нему, тогда может наблюдаться только часть этой записи, что дает вам совершенно другое значение (например, вы обновляете i с 3 до 4 а другой поток читает 7). По общему признанию, это маловероятно с bool. Но стандарт еще шире, чем этот: это неопределенное поведение , поэтому он может даже sh взломать вашу программу или иметь какое-то другое странное неожиданное поведение.

2 голосов
/ 11 июля 2020

Да, скорее всего, сработает, но только «случайно». Как уже писал @ idclev463035818:

std :: atomi c не для "сложных случаев". Это когда вам нужно получить доступ к чему-то из нескольких потоков.

Поэтому в этом случае вы должны использовать atomic<bool> вместо volatile. Тот факт, что volatile был частью этого языка задолго до появления потоков в C ++ 11, уже должен быть убедительным свидетельством того, что volatile никогда не проектировался и не предназначался для использования для многопоточности. Важно отметить, что в C ++ volatile в корне отличается от volatile в таких языках, как Java или C#, где volatile фактически связано с моделью памяти. В этих языках переменная volatile очень похожа на atomi c в C ++.

В C ++ volatile используется для того, что часто называют «необычной памятью», где память может быть прочитана или изменен вне текущего процесса (например, при использовании ввода-вывода с отображением памяти). volatile заставляет компилятор выполнять все операции в точном порядке, как указано. Это предотвращает некоторые оптимизации, которые были бы совершенно законными для атомиков, а также допускает некоторые оптимизации, которые фактически незаконны для атомиков. Например:

volatile int x;
         int y;
volatile int z;

x = 1;
y = 2;
z = 3;
z = 4;

...

int a = x;
int b = x;
int c = y;
int d = z;

В этом примере есть два назначения для z и две операции чтения для x. Если бы x и z были атомными, а не изменчивыми, компилятор мог бы увидеть первое хранилище как несущественное и просто удалить его. Точно так же он может просто повторно использовать значение, возвращенное первой загрузкой x, эффективно сгенерировать код вроде int b = a. Но поскольку x и z изменчивы, эти оптимизации невозможны. Вместо этого компилятор должен гарантировать, что все изменчивые операции выполняются в точном порядке , как указано, т. Е. Изменчивые операции не могут быть переупорядочены относительно друг друга. Однако это не мешает компилятору переупорядочивать энергонезависимые операции. Например, операции с y можно было бы свободно перемещать вверх или вниз - что было бы невозможно, если бы x и z были атомными. Поэтому, если бы вы попытались реализовать блокировку на основе изменчивой переменной, компилятор мог бы просто (и законно) переместить некоторый код за пределы критической секции. volatile не мешает ему участвовать в гонке данных. В тех редких случаях, когда у вас есть некоторая «необычная память» (и, следовательно, действительно требуется volatile), к которой также обращаются несколько потоков, вы должны использовать volatile atomics.

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