Подтвердить что ты не робот

Обработка неработоспособности

Недавно я наткнулся на эту статью в Википедии. Из моего опыта многопоточности я знаю множество проблем, вызванных тем, что программа может в любой момент переключать потоки между потоками. Тем не менее, я никогда не знал, что оптимизация компилятора и аппаратного обеспечения может переупорядочить операции таким образом, что гарантированно будет работать для одного потока, но не обязательно для многопоточности. Может ли кто-нибудь объяснить, как правильно иметь дело с возможностью переупорядоченных операций в многопоточной среде?

ОБНОВЛЕНИЕ. Я изначально случайно связался с статьей Внештатный вариант вместо Проблема с защитой памяти, которая лучше объясняет проблему.

4b9b3361

Ответ 1

Я рассмотрю ваш вопрос как вопрос о многопоточности на языке высокого уровня, а не о обсуждении оптимизации конвейера ЦП.

Может кто-нибудь объяснить, как правильно иметь дело с возможностью переупорядоченных операций в многопоточной среде?

Большинство, если не все, современных высокоуровневых многопоточных языков предоставляют конструкции для управления этим потенциалом для компилятора, чтобы изменить порядок логического выполнения инструкций. В С# они включают в себя конструкции на уровне поля (модификатор volatile), конструкции на уровне блоков (lock keyword) и императивные конструкции (Thead.MemoryBarrier).

Применение поля volatile приводит к тому, что весь доступ к этому полю в CPU/памяти выполняется в том же относительном порядке, в котором он встречается в последовательности команд (исходный код).

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

Метод Thread.MemoryBarrier указывает компилятору, что ЦП не должен изменять порядок доступа к памяти в этой точке последовательности команд. Это позволяет использовать более совершенный метод для специализированных требований.

Приведенные выше методы описаны в порядке возрастания сложности и производительности. Как и во всех программах concurrency, определение того, когда и где применять эти методы, является задачей. При синхронизации доступа к одному полю ключевое слово volatile будет работать, но это может оказаться излишним. Иногда вам нужно только синхронизировать записи (в этом случае a ReaderWriterLockSlim выполнит то же самое с гораздо лучшей производительностью). Иногда вам нужно манипулировать поле несколько раз подряд, или вы должны проверить поле и условно манипулировать им. В этих случаях ключевое слово lock является лучшей идеей. Иногда у вас есть несколько потоков, управляющих общим состоянием в очень слабо синхронизированной модели для повышения производительности (обычно не рекомендуется). В этом случае тщательно размещенные барьеры памяти могут препятствовать использованию устаревших и непоследовательных данных в потоках.

Ответ 2

Позвольте мне задать вопрос: учитывая программный код (скажем, это однопоточное приложение), каково правильное выполнение? Интуитивно, выполнение ЦП в порядке, как указано в коде, было бы правильным. Эта иллюзия последовательного выполнения - это то, что есть у программистов.

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

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

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

(Чтобы узнать больше, я рекомендую вам взглянуть на концепцию ILP (уровень инструкций parallelism). производительность потоков в основном зависит от того, сколько ILP вы можете извлечь из одного потока. Таким образом, как процессоры, так и компиляторы делают все возможное для лучшей производительности.)

Однако, когда вы рассматриваете многопоточное выполнение, у него есть потенциальная проблема, называемая проблемой согласованность памяти. Интуитивно программисты имеют концепцию последовательной согласованности. Однако современные многоядерные архитектуры выполняют грязную и агрессивную оптимизацию (например, кэши и буферы). Трудно реализовать последовательную согласованность с низкими накладными расходами в современной компьютерной архитектуре. Таким образом, может возникнуть очень запутанная ситуация из-за внештатных исполнений нагрузок и хранилищ памяти. Вы можете наблюдать за некоторыми грузами и магазинами, выполненными не по порядку. Ознакомьтесь с некоторыми статьями, относящимися к моделях с ослабленной памятью, например Модель памяти Intel x86 (прочтите главу 8 "Порядок памяти" тома 3А Intel 64 и IA -32 Руководство разработчика программного обеспечения для архитектур). В этой ситуации необходимы блокировки памяти, где вам необходимо обеспечить выполнение инструкций по правилу памяти.

ОТВЕТ НА ВОПРОС. Нелегко ответить на этот вопрос короче. Нет никаких хороших инструментов, которые бы выявляли бы такое неудобное и проблемное поведение из-за модели согласованности памяти (хотя есть научные статьи). Итак, короче говоря, вам даже сложно найти такие ошибки в вашем коде. Тем не менее, я настоятельно рекомендую вам прочитать статьи о дважды проверенной блокировке и подробный документ. В двойной проверке блокировки из-за расслабленной согласованности памяти и переупорядочения компиляторов (обратите внимание, что компиляторы не знают многопоточное поведение, если вы явно не указали с барьерами памяти), это может привести к неправильному поведению.

В сумме:

  • Если вы работаете только с однопоточной программой, вам не нужно беспокоиться о поведении не по порядку.
  • На многоядерном процессоре вам может потребоваться рассмотреть проблемы согласованности памяти. Но это действительно редко, когда вам действительно нужно беспокоиться о проблеме согласованности памяти. В основном, расы данных, тупики и нарушения атомарности убивают вашу многопоточную программу.

Ответ 3

Позвольте быть понятным - выход из строя относится к конвейеру выполнения процессора не к самому компилятору, поскольку ваша ссылка наглядно демонстрирует. Вне исполнения заказа - это стратегия, применяемая большинством современных процессорных конвейеров, что позволяет им переупорядочивать инструкции "на лету", чтобы, как правило, минимизировать стойки чтения/записи, которые являются наиболее распространенным узким местом на современном оборудовании из-за несоответствия между скоростями выполнения ЦП и памятью латентность (то есть, как быстро мой процессор может извлекать и обрабатывать по сравнению с тем, как быстро я могу обновить результат обратно в ОЗУ).
Таким образом, это прежде всего аппаратная функция, а не функция компилятора.
Вы можете переопределить эту функцию, если знаете, что обычно делаете, используя барьеры памяти. Power PC имеет замечательно названную инструкцию под названием eieio (принудительное выполнение операций ввода-вывода), которое заставляет CPU очищать все ожидающие чтения и записи в память - это особенно важно при параллельном программировании (будь то многопоточный или многопоточный, процессор), поскольку он гарантирует, что все процессоры или потоки синхронизировали значение всех мест памяти.
Если вы хотите подробно прочитать об этом, то этот PDF - отличное (хотя и подробное) введение.
HTH

Ответ 4

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

И ЦП, и компилятор оптимизируются на основе того же правила: переупорядочивайте что угодно, пока оно не влияет на результаты программы * при условии однопоточной однопроцессорной среды *.

Итак, есть проблема - она ​​оптимизируется для однопоточности, когда это не так. Зачем? Потому что иначе все будет на 100 раз медленнее. действительно. И большая часть вашего кода является однопоточным (т.е. однопоточное взаимодействие) - только мелкие части должны взаимодействовать многопоточным способом.

Лучший/самый простой/безопасный способ управления этим - блокировки - мьютексы, семафоры, события и т.д.

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

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

Ответ 5

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

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

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

Ответ 6

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

Ответ 7

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

Поскольку ни C, ни С++ не имеют сильно определенной модели памяти, компиляторы могут переупорядочить оптимизацию, которая может вызвать проблемы для многопоточности. Но что касается компиляторов, которые предназначены для использования в многопоточных средах, они этого не делают.

Многопоточный код либо записывает в память, либо использует забор для обеспечения видимости записи между потоками или использует атомарные операции.

Поскольку значения, используемые в случае работы атома, наблюдаются в одном потоке, переупорядочение не влияет на это - они должны быть правильно рассчитаны до атомной операции.

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

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

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

Ответ 8

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

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

В ответ на ваше название: вы не выполняете выполнение ООО.

Ответ 9

Значит, вы спрашиваете о модели согласованности памяти. Некоторые языки/среды, такие как Java и .NET, определяют модель памяти, и ответственность программиста заключается в том, чтобы не делать то, что не разрешено, или приводит к поведению undefined. Если вы не уверены в поведении атомарности "нормальных" операций, лучше быть в безопасности, чем сожалеть, и просто использовать примитивы mutex.

Для C и С++ ситуация не такая приятная, как эти языковые стандарты не определяют модель памяти. И нет, вопреки общепринятому мнению, волатильность не гарантирует ничего атомарности. В этом случае вы должны полагаться на библиотеку потоков платформ (которая, помимо прочего, выполняет требуемые барьеры памяти), или на атомарную внутреннюю среду, специфичную для компилятора /hw, и надеемся, что компилятор не выполняет никаких оптимизаций, которые нарушают семантику программы. Если вы избегаете условной блокировки внутри функции (или единицы перевода при использовании IPA), вы должны быть относительно безопасными.

К счастью, С++ 0x и следующий стандарт C исправляют эту проблему, определяя модель памяти. Я задал вопрос, связанный с этим, и, как оказалось, условная блокировка здесь; вопрос содержит ссылки на некоторые документы, которые подробно рассматриваются в этой проблеме. Я рекомендую вам прочитать эти документы.

Ответ 10

Как вы предотвращаете возможность выполнения функций выполнения и взорвавшихся в вашем лице?

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

Ответ 11

Большинство компиляторов в настоящее время имеют явные упорядочивающие функции. С++ 0x также имеет функции упорядочения памяти.

Ответ 12

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