Нужно ли 'volatile' в этом многопоточном коде C ++? - PullRequest
11 голосов
/ 31 августа 2010

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

КОД, ВЫПОЛНЕННЫЙ ПОТОКОМ 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 действительно никак не обеспечивает безопасность потока - но я хотел спросить, если приведенный выше код демонстрирует правильное поведение, гарантирующее, что программа является потокобезопасной.

Ответы [ 8 ]

14 голосов
/ 31 августа 2010

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

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

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

9 голосов
/ 31 августа 2010

Википедия говорит, что это очень хорошо.

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

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

3 голосов
/ 01 сентября 2010

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

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

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

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

3 голосов
/ 31 августа 2010

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

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

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

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

1 голос
/ 01 сентября 2010

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

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

0 голосов
/ 01 сентября 2010

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

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

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

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

0 голосов
/ 01 сентября 2010

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

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

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

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

0 голосов
/ 01 сентября 2010

Хотя остальные ответы верны, я также предлагаю вам взглянуть на раздел «Специфично для Microsoft» документации MSDN для volatile

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