Безопасное перенос данных только для чтения в новый поток

Предположим, что у меня есть программа, которая инициализирует глобальную переменную для использования потоками, например:

int ThreadParameter;

// this function runs from the main thread
void SomeFunction() {
    ThreadParameter = 5;

    StartThread(); // some function to start a thread
    // at this point, ThreadParameter is NEVER modified.
}

// this function is run in a background worker thread created by StartThread();
void WorkerThread() {
    PrintValue(ThreadParameter); // we expect this to print "5"
}

Эти вопросы должны применяться для любой общей архитектуры процессора, с которой можно столкнуться. Я хочу, чтобы решение было портативным - не специфичным для архитектуры с более надежными гарантиями памяти, такими как x86.

  • Общий вопрос: несмотря на то, что он очень распространен, действительно ли это безопасно для всех процессорных архитектур? Как сделать это безопасным, если нет?
  • Глобальная переменная не volatile; возможно, он будет переупорядочен после вызова StartThread() и оставить меня в покое? Как исправить эту проблему?
  • Предположим, что на компьютере есть два процессора, у которых есть свои собственные кеши. Основной поток работает на первом процессоре, а рабочий поток работает на втором процессоре. Предположим, что блок памяти, содержащий ThreadParameter, был загружен в каждый кеш процессора до начала запуска программы SomeFunction(). SomeFunction() записывает 5 в ThreadParameter, который сохраняется в первом кэше процессора, а затем запускает рабочий поток, который выполняется на втором процессоре. Не будет ли WorkerThread() для второго процессора видеть неинициализированные данные для ThreadParameter вместо ожидаемого значения 5, так как страница памяти второго процессора еще не увидела обновления от первого процессора?
  • Если требуется что-то другое - как лучше всего справиться с этим, а не простым int, я мог бы работать с указателем на гораздо более сложные типы данных, которые не обязательно используются в многопоточной среде?

Если мои опасения необоснованны, каковы конкретные причины, по которым мне не нужно беспокоиться?

Ответ 1

Из вашего описания, кажется, вы пишете ThreadParameter (или какую-то другую структуру данных) перед началом любых дочерних потоков, и вы больше никогда не будете писать в ThreadParameter... он существует для чтения по мере необходимости, но никогда не менялся снова после его инициализации; это верно? Если это так, тогда нет никакой необходимости использовать любые системные вызовы нисходящей синхронизации (или примитивы процессоров/компиляторов) каждый раз, когда дочерний поток хочет прочитать данные или даже в первый раз в этом отношении.

Взаимодействие с изменчивостью несколько специфично для компилятора; Я знаю, что, по крайней мере, с Diab для PowerPC, есть опция компилятора относительно обработки volatile: либо используйте инструкцию PowerPC EIEIO (или MBAR) после каждого чтения/записи в переменную, либо не используйте ее... это в дополнение к запрету оптимизаций компилятора, связанных с переменной. (EIEIO/MBAR является инструкцией PowerPC для запрета переупорядочивания ввода-вывода самим процессором, то есть все операции ввода-вывода перед тем, как инструкция должна завершиться до ввода ввода-вывода после команды).

С точки зрения правильности/безопасности, это не помешает объявить его изменчивым. Но с прагматичной точки зрения, если вы инициализируете ThreadParameter достаточно далеко перед StartThread(), объявление его volatile не должно действительно быть необходимым (и не делать этого ускорит все последующие его обращения). В значительной степени любой существенный вызов функции (скажем, возможно, для printf() или cout, или любой системный вызов и т.д.) Будет выдавать на порядки больше инструкций, чем необходимо, чтобы гарантировать, что процессор так и не обработал бы ThreadParameter перед вызовом StartThread(). Реально, сам StartThread() почти наверняка выполнит достаточное количество инструкций, прежде чем соответствующий поток начнется. Поэтому я предлагаю вам не обязательно объявлять его изменчивым, возможно, даже если вы его инициализируете непосредственно перед вызовом StartThread().

Теперь о вашем вопросе о том, что произойдет, если страница, содержащая эту переменную, уже загружена в кеш обоих процессоров, прежде чем процессор, выполняющий основной поток, выполнит инициализацию: если вы используете общедоступную платформу общего назначения с аналогичные процессоры, аппаратное обеспечение уже должно быть на месте для обработки когерентности кэша для вас. Место, где возникают проблемы с когерентностью кэш-памяти на платформах общего назначения, независимо от того, много они или нет, - это когда ваш процессор имеет отдельные кэши команд и данных, и вы пишете самомодифицирующийся код: инструкции, написанные в память, неотличимы от данных, поэтому CPU не делает недействительными эти местоположения в кэше команд, поэтому в кэше команд могут быть устаревшие инструкции, если вы впоследствии не сделаете недействительными эти местоположения в кэше команд (либо выдаете свои собственные инструкции по сборке процессора, которые вы, возможно, не будете разрешено делать в зависимости от вашей ОС и уровня привилегий потока, а также для выдачи соответствующего системного вызова с кэшем-недействительным для вашей ОС). Но то, что вы описываете, не является самомодифицирующимся кодом, поэтому вы должны быть в этом уверены.

В вашем вопросе 1 спрашивается, как сделать это безопасным во всех архитектурах процессоров. Ну, как я уже говорил выше, вы должны быть в безопасности, если используете аналогичные процессоры, чьи шины данных должным образом перекрыты. Процессоры общего назначения, предназначенные для многопроцессорного соединения, имеют протоколы SNOOP для обнаружения записи в общую память... до тех пор, пока ваша библиотека потоков правильно настроит область разделяемой памяти. Если вы работаете во встроенной системе, вам, возможно, придется настроить это самостоятельно в своем BSP... для PowerPC, вам нужно посмотреть бит WIMG в конфигурации MMU/BAT; Я не знаком с другими архитектурами, чтобы дать вам указания на них. НО... Если ваша система доморощенная или если ваши процессоры не похожи друг на друга, возможно, вы не сможете рассчитывать на то, что два процессора смогут отслеживать записи друг друга; обратитесь к вашим специалистам по аппаратным средствам за советом.

Ответ 2

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

(Компилятор должен убедиться, что все записи, сделанные до начала потока, видны в новом потоке.)

Ответ 3

  • Да, это безопасно.
  • Не знаю. Может быть: if( ThreadParameter = 5 ) StartThread();. Однако, в общем, постарайтесь не догадываться о компиляторе.
  • Наверное, нет. Если вам приходится беспокоиться о таких деталях низкого уровня при написании кода, то логика, которая контролирует выполнение программы на многоядерной машине, вероятно, не очень хорошо выполняет свою работу.
  • Boost - ваш друг для работы со сложными типами в многопоточной среде.