Является ли "volatile" необходимым в этом многопоточном коде С++?

Я написал программу Windows на С++, которая порой использует два потока: один фоновый поток для выполнения трудоемкой работы; и другой поток для управления графическим интерфейсом. Таким образом, программа по-прежнему реагирует на пользователя, что необходимо для того, чтобы прервать определенную операцию. Потоки обмениваются данными через общую переменную bool, которая установлена ​​в true, когда поток GUI сигнализирует прерыванию рабочего потока. Вот код, который реализует это поведение (я удалил ненужные части):

КОД, ВЫПОЛНЕННЫЙ РЕЖИМАМИ GUI


class ProgressBarDialog : protected Dialog {

    /**
     * This points to the variable which the worker thread reads to check if it
     * should abort or not.
     */
    bool volatile* threadParameterAbort_;

    ...

    BOOL CALLBACK ProgressBarDialog::DialogProc( HWND dialog, UINT message, 
        WPARAM wParam, LPARAM lParam ) {

        switch( message ) {
            case WM_COMMAND :
                switch ( LOWORD( wParam ) ) {

                    ...

                    case IDCANCEL :
                    case IDC_BUTTON_CANCEL :
                        switch ( progressMode_ ) {
                            if ( confirmAbort() ) {
                                // This causes the worker thread to be aborted
                                *threadParameterAbort_ = true;
                            }
                            break;
                        }

                        return TRUE;
                }
        }

        return FALSE;
    }

    ...

};

КОД, ВЫПОЛНЕННЫЙ РЕЗЬБОЙ РАБОТНИКА


class CsvFileHandler {

    /**
     * This points to the variable which is set by the GUI thread when this
     * thread should abort its execution.
     */
    bool volatile* threadParamAbort_;

    ...

    ParseResult parseFile( ItemList* list ) {
        ParseResult result;

        ...

        while ( readLine( &line ) ) {
            if ( ( threadParamAbort_ != NULL ) && *threadParamAbort_ ) {
                break;
            }

            ...
        }

        return result;
    }

    ...

};

threadParameterAbort_ в обоих потоках указывает на переменную bool, объявленную в структуре, которая передается рабочему потоку при создании. Он объявлен как

bool volatile abortExecution_;

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

  • предотвратить чтение *threadParameterAbort_ для использования кеша и вместо этого получить значение из памяти и

  • запретить компилятору удалять предложение if в рабочем потоке из-за оптимизации.

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

РЕДАКТИРОВАТЬ: Небольшая ошибка в моей формулировке вызвала вопрос, как будто я спрашивал, достаточно ли volatile для обеспечения безопасности потоков. Это не мое намерение - volatile действительно не обеспечивает безопасность потоков в любом случае, но я хотел спросить, был ли приведенный выше код демонстрирует правильное поведение, чтобы гарантировать, что программа является потокобезопасной.

Ответ 1

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

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

Volatile следует использовать только для IO с отображением памяти, где несколько считываний могут возвращать разные значения. Аналогично для записанных в память записей.

Ответ 2

Wikipedia говорит об этом довольно хорошо.

В C и, следовательно, С++, ключевое слово volatile предназначалось для обеспечения доступа к устройствам с отображением памяти, разрешающим использование переменных между setjmp разрешать использование переменных sig_atomic_t в обработчиках сигналов

Операции с изменчивыми переменными не атомарно, не происходит-до отношения для резьб. Это согласно соответствующие стандарты (C, С++, POSIX, WIN32), и это факт для подавляющего большинства текущих Реализации. Неустойчивый ключевое слово в принципе бесполезно, поскольку переносной конструкцией резьбы.

Ответ 3

volatile не является ни необходимым, ни достаточным для многопоточности в С++. Он отключает оптимизацию, которая является вполне приемлемой, но не позволяет применять такие вещи, как атомарность, которая необходима.

Изменить: вместо использования критического раздела я бы, вероятно, использовал InterlockedIncrement, который дает атомную запись с меньшими накладными расходами.

Однако, что я обычно делаю, подключайте потокобезопасную очередь (или deque) в качестве входа в поток. Когда у вас есть что-то для потока, вы просто кладете пакет данных, описывающих задание, в очередь, и поток делает это, когда это возможно. Если вы хотите, чтобы поток был отключен нормально, вы помещаете пакет "shutdown" в очередь. Если вам нужна немедленная остановка, вы вместо этого используете deque и поместите команду "abort" на переднюю часть deque. Теоретически это имеет недостаток, что он не прерывает поток, пока он не завершит свою текущую задачу. Все это означает, что вы хотите, чтобы каждая задача была примерно того же диапазона размеров и латентности, что и частота, с которой вы сейчас проверяете флаг.

Этот общий дизайн позволяет избежать целого множества проблем IPC.

Ответ 4

Что касается моего ответа на вчерашний вопрос, нет, volatile не нужно. На самом деле многопоточность здесь не имеет значения.

    while ( readLine( &line ) ) { // threadParamAbort_ is not local:
        if ( ( threadParamAbort_ != NULL ) && *threadParamAbort_ ) {
  • предотвратить чтение * threadParameterAbort_ для использования кеша и вместо этого получить значение из памяти и
  • запретить компилятору удалять условие if в рабочем из-за оптимизации.

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

Итак, компилятор предполагает, что readLine имеет свой собственный private static bool * to threadParamAbort_ и изменяет значение. Поэтому необходимо перезагрузить память.

Ответ 5

Кажется, что здесь используется один и тот же вариант использования: volatile - Многопоточный программист Лучший друг от Alexandrescu. Он утверждает, что именно в этом случае (для создания флага) volatile может быть отлично использован.

Итак, да именно в этом случае код должен быть правильным. volative предотвратит оба чтения из кеша и не позволит компилятору оптимизировать инструкцию if.

Ответ 7

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

Главное, что volatile будет делать, - это предотвратить кэширование или извлечение переменной из реестра. Вы можете быть уверены, что он поступает из основной памяти.

Итак, если бы я был вами, да, я бы определил переменную как изменчивую.

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

Ответ 8

Хорошо, так что вы были достаточно избиты относительно летучих и безопасности потоков!, но...

Пример вашего конкретного кода (хотя и растягивается и находится под вашим контролем) заключается в том, что вы просматриваете свою переменную несколько раз в одной транзакции:

if ( ( threadParamAbort_ != NULL ) && *threadParamAbort_ )

Если по какой-либо причине threadParamAbort_ удаляется после левой стороны и перед правой стороной, вы будете разыскивать удаленный указатель. Опять же, это маловероятно, если у вас есть контроль, но это пример того, что волатильность и атомарность не могут сделать для вас.