Сотрудничество потоков на двухпроцессорных машинах
Я помню, что в курсе, который я брал в колледже, одним из моих любимых примеров состояния гонки был случай, когда main()
Метод запускает два потока, один из которых увеличивает общую (глобальную) переменную на один, а другой уменьшает ее. Псевдокод:
static int i = 10;
main() {
new Thread(thread_run1).start();
new Thread(thread_run2).start();
waitForThreads();
print("The value of i: " + i);
}
thread_run1 {
i++;
}
thread_run2 {
i--;
}
Затем профессор спросил, какова ценность i
после миллиона миллиардов миллиардов пробежек. (Если это вообще что-то отличное от 10). Учащиеся, незнакомые с многопоточными системами, отвечали, что в 100% случаев print()
заявление всегда будет сообщать i
как 10.
Это было на самом деле неверно, так как наш профессор продемонстрировал, что каждое утверждение приращения / убывания фактически было скомпилировано (для сборки) как 3 утверждения:
1: move value of 'i' into register x
2: add 1 to value in register x
3: move value of register x into 'i'
Таким образом, значение i
может быть 9, 10 или 11. (Я не буду вдаваться в подробности.)
Мой вопрос:
Было (есть?) Мое понимание того, что набор физических регистров зависит от процессора. При работе с двухпроцессорными компьютерами (обратите внимание на разницу между двухъядерным и двухпроцессорным процессами), имеет ли каждый ЦП свой набор физических регистров? Я предполагал, что ответ - да.
На однопроцессорной (многопоточной) машине переключение контекста позволяет каждому потоку иметь собственный виртуальный набор регистров. Поскольку на двухпроцессорной машине имеется два физических набора регистров, это не может привести к еще большему потенциалу для состязания, так как вы можете буквально иметь два потока, работающих одновременно, в отличие от "виртуальной" одновременной работы на одном компьютере. Процессор? (Виртуальная одновременная операция в связи с тем, что регистровые состояния сохраняются / восстанавливаются при каждом переключении контекста.)
Чтобы быть более конкретным - если вы выполняли это на 8-процессорной машине, каждый процессор с одним потоком, исключены ли условия гонки? Если вы расширите этот пример для использования 8 потоков на двухпроцессорной машине, каждый из которых имеет 4 ядра, увеличится или уменьшится потенциал условий гонки? Как операционная система предотвращает step 3
инструкции по сборке от одновременного запуска на двух разных процессорах?
3 ответа
Да, введение двухъядерных процессоров привело к быстрому провалу значительного числа программ с скрытыми потоками потоков. Многоядерные одноядерные процессоры планировщиком быстро переключают контекст потоков между потоками. Что устраняет класс ошибок потоков, связанных с устаревшим кэшем ЦП.
Однако приведенный вами пример может не работать на одном ядре. Когда планировщик потока прерывает поток так же, как он загрузил значение переменной в регистр, чтобы увеличить его. Он просто не будет терпеть неудачу почти так же часто, потому что вероятность того, что планировщик прерывает поток, просто не так велика.
Существует функция операционной системы, позволяющая этим программам в любом случае хромать, а не зависать в течение нескольких минут. Вызывается "привязка к процессору", доступно как параметр командной строки AFFINITY для start.exe в Windows, SetProcessAfinityMask() в winapi. Просмотрите класс Interlocked для вспомогательных методов, которые атомарно увеличивают и уменьшают переменные.
У вас все еще было бы состояние гонки - это ничего не меняет. Представьте, что два ядра одновременно выполняют инкремент - они оба загружают одно и то же значение, инкремент к одному и тому же значению, а затем сохраняют одно и то же значение... так что общий прирост от двух операций будет один вместо двух,
Существуют дополнительные причины потенциальных проблем, когда речь идет о моделях памяти - когда шаг 1 может на самом деле не получить последнее значение i
и шаг 3 может не сразу записать новое значение i
таким образом, что другие темы могут видеть.
По сути, все становится очень сложно - вот почему, как правило, хорошей идеей является либо использовать синхронизацию при доступе к общим данным, либо использовать не требующие блокировки абстракции более высокого уровня, написанные экспертами, которые действительно знают, что они делают.
Во-первых, двойной процессор по сравнению с двухъядерным не имеет реального эффекта. Двухъядерный процессор все еще имеет два совершенно разных процессора на чипе. Они могут совместно использовать некоторый кэш и совместно использовать общую шину для памяти / периферийных устройств, но сами процессоры полностью разделены. (Двунаправленный однопоточный код, такой как Hyperthreading) - это третий вариант, но он также имеет набор регистров для каждого виртуального процессора. Два процессора совместно используют один набор ресурсов выполнения, но они сохраняют совершенно отдельные наборы регистров.
Во-вторых, на самом деле есть только два случая, которые действительно интересны: один поток выполнения и все остальное. Если у вас более одного потока (даже если все потоки выполняются на одном процессоре), у вас возникают те же потенциальные проблемы, как если бы вы работали на огромной машине с тысячами процессоров. Теперь, безусловно, верно, что вы, скорее всего, увидите, что проблемы проявляются намного раньше, когда код работает на большем количестве процессоров (вплоть до того, сколько вы создали потоков), но сами проблемы не имеют / не делают изменить вообще.
С практической точки зрения наличие большего количества ядер полезно с точки зрения тестирования. Учитывая гранулярность переключения задач на типичной ОС, довольно легко написать код, который будет работать годами, не показывая проблем на одном процессоре, который вылетит и сработает в течение нескольких часов или даже минут, если вы запустите его на двух других или физические процессоры. Однако проблема на самом деле не изменилась - просто вероятность появления гораздо быстрее, когда у вас больше процессоров.
В конечном счете, состояние гонки (или взаимоблокировка, живая блокировка и т. Д.) Касается дизайна кода, а не оборудования, на котором он работает. Аппаратное обеспечение может повлиять на то, какие шаги необходимо предпринять для обеспечения выполнения соответствующих условий, но соответствующие различия имеют мало общего с простым числом процессоров. Скорее, речь идет о таких вещах, как уступки, когда у вас есть не просто одна машина с несколькими процессорами, а несколько машин с совершенно разными адресными пространствами, поэтому вам, возможно, придется предпринять дополнительные шаги, чтобы при записи значения в память он становится видимым для процессоров на других машинах, которые не могут видеть эту память напрямую.