Координация Шанхайского парка высоких технологий Songjiang, мы ищем старших интерфейсных инженеров / старших инженеров Java, если вы заинтересованы, см. JD:Lagoo.com/Jobs/636156…
Основа параллелизма и модель памяти необходимой серии одновременных интервью
существует«Потрясающие интервью»Among the common interview questions summarized, the knowledge of concurrency and asynchrony is the top priority of the interview, regardless of the front-end and back-end. This series is to review and summarize the common concurrency knowledge in the interview; you can also перейти к«Потрясающие интервью», в реальной школе вопросы теста интервью, чтобы понять их мастерство. Вы также можете перейти кЯва в действии", "Перейти в действие》 и т. д., чтобы понять соответствующие знания о параллельном программировании на конкретных языках программирования.
С быстрым развитием производительности оборудования и наступлением эры больших данных, чтобы заставить код работать быстрее, полагаясь только на более быстрое оборудование, больше нельзя удовлетворять требованиям.Параллельные и распределенные вычисления являются основным содержанием современных приложений; нам нужно использовать несколько одноядерных или несколько машин для ускорения приложений или запуска их в масштабе, параллельное программирование все больше становится важной частью программирования, которую нельзя игнорировать.
Начиная с простого определения исполнительные блоки являются параллельными, если их логический поток управления перекрывается во времени; это определение может быть расширено до очень широкого круга понятий, которые зависят от операционной системы, хранилища и т. д., системы распределения, микросервисов и т. д. и т. д., и, в частности, он попадет в области параллельного программирования на Java, параллельного программирования на Go и асинхронного программирования на JavaScript. Облачные вычисления обещают бесконечную масштабируемость во всех измерениях (память, вычисления, хранилище и т. д.), а параллельное программирование и связанные с ним теории также являются основой для создания крупномасштабных распределенных приложений.
Параллельность и параллельность
Параллелизм — это программа, которая может выполняться одновременно, в отношении логической структуры программы; параллелизм — это параллельная программа, которая может выполняться на оборудовании, поддерживающем параллелизм, в отношении текущего состояния программы. Другими словами, параллельные программы представляют собой все программы, которые могут обеспечивать параллельное поведение Это относительно широкое понятие, и параллельные программы являются лишь его подмножеством. Параллелизм является необходимым условием параллелизма, но параллелизм не является достаточным условием для параллелизма. Concurrency — это просто выражение, которое больше соответствует сути реальных проблем, цель — упростить логику кода, а не ускорить работу программы. Если программа работает быстрее, это должны быть параллельные программы плюс многоядерный параллелизм.
Короче говоря, параллелизм — это способность работать с несколькими задачами одновременно, а параллелизм — это способность выполнять несколько задач одновременно.
Параллелизм — это концепция проблемной области: программы должны быть разработаны для обработки нескольких одновременных (или почти одновременных) событий; параллельная программа содержит несколько логически независимых блоков выполнения, которые могут выполняться независимо параллельно или последовательно. Параллелизм, с другой стороны, является концепцией в области методов — ускорение решения проблемы за счет параллельного выполнения нескольких частей задачи. Параллельная программа имеет тенденцию решать проблемы намного быстрее, чем последовательная программа, потому что она может выполнять несколько частей общей задачи одновременно. Параллельная программа может иметь несколько независимых блоков выполнения или только один.
В частности, ранний Redis (многопоточность также была введена после версии 6.0) был бы хорошим примером разграничения параллелизма и параллелизма.Это сама по себе однопоточная база данных, но ее можно мультиплексировать с циклом событий. услуги ИО. Это связано с тем, что многоядерный параллелизм по своей природе требует больших затрат на синхронизацию, особенно в случае блокировок или семафоров. Поэтому Redis использует однопоточный цикл обработки событий, чтобы гарантировать серию атомарных операций, тем самым обеспечивая синхронизацию с почти нулевым потреблением даже в случае высокого параллелизма. Еще раз процитируем описание Роба Пайка:
A single-threaded program can definitely provides concurrency at the IO level by using an IO (de)multiplexing mechanism and an event loop (which is what Redis does).
Измерение параллелизма
параллелизм на уровне потоков
С момента появления обмена временем в начале 1960-х годов компьютерные системы имели поддержку одновременного исполнения; традиционно такое одновременное исполнение моделировалось только путем быстрого переключения компьютера в конфигурации, называемой системой Uniprocessor. Начиная в 1980-х годах, многопроцессорных систем, то есть системы, состоящие из нескольких процессоров, контролируемых единой системой операционной системы, используемые технологии, такие как многоядерные процессоры и гиперторизация, что позволило нам достичь истинного параллелизма. Многоядерный процессор объединяет несколько процессоров на одну интегральную микросхему.
Гиперпоточность, иногда называемая одновременной многопоточностью, представляет собой технологию, которая позволяет одному процессору выполнять несколько потоков управления. Это предполагает, что ЦП имеет несколько копий некоторых аппаратных средств, таких как программные счетчики и регистровые файлы, в то время как другие части аппаратного обеспечения имеют только одну копию, например устройство, выполняющее арифметические операции с плавающей запятой. Обычному процессору требуется около 20 000 тактовых циклов для переключения между потоками, в то время как процессор с гиперпоточностью может решить, какой поток выполнять за один цикл. Это позволяет процессору лучше использовать свои вычислительные ресурсы. Например, предположим, что один поток должен дождаться загрузки некоторых данных в кэш, после чего ЦП может продолжить выполнение другого потока.
параллелизм на уровне инструкций
На более низком уровне абстракции свойство современных процессоров выполнять несколько инструкций одновременно называется параллелизмом на уровне инструкций. На самом деле каждая инструкция от начала до конца занимает гораздо больше времени, порядка 20 и более циклов, но процессор использует множество хитрых уловок, чтобы обрабатывать до 100 инструкций одновременно. При конвейерной обработке действия, необходимые для выполнения инструкции, делятся на разные этапы, а аппаратное обеспечение процессора организовано в ряд этапов, каждый из которых выполняет определенный шаг. Эти этапы могут работать параллельно для обработки разных частей разных инструкций. Мы увидим довольно простую аппаратную конструкцию, которая может обеспечить скорость выполнения, близкую к одной инструкции за такт. Если процессор может достичь более высокой скорости выполнения, чем одна инструкция за такт, он называется суперскалярным (Super Scalar) процессором.
Одна инструкция, несколько данных
На самом низком уровне многие современные процессоры имеют специальное оборудование, которое позволяет одной инструкции выполнять несколько операций, которые могут выполняться параллельно, что называется параллелизмом с одной инструкцией, несколькими данными или SIMD. Например, более новые процессоры Intel и AMD имеют инструкции для параллельного добавления 4 пар чисел с плавающей запятой одинарной точности (тип данных C float).
Синхронный, асинхронный, блокирующий, неблокирующий
После основных концепций параллелизма и параллелизма нам также необходимо понять взаимосвязь и разницу между понятиями синхронизации, асинхронности, блокировки и неблокировки.
Синхронизация означает, что после запуска операции она ожидает ее завершения до окончания операции, асинхронность означает, что она уходит сразу после выполнения операции, и если позже будет ответ, исполнитель будет уведомлен. С точки зрения программирования, при синхронном вызове результат вызова будет возвращен после этого вызова. При асинхронном вызове результат вызова не возвращается напрямую. Объект Future или Promise будет возвращен вызывающей стороне для активного/пассивного получения результата этого вызова.
В параллельном программировании блокировка и неблокировка в основном различаются с точки зрения гонки доступа к общим ресурсам в критических разделах или к общим данным. Общие ресурсы, необходимые для операции, заняты, и их можно только ожидать, что называется блокировкой; когда общие ресурсы, необходимые для операции, заняты, она возвращается немедленно, не дожидаясь, и возвращается с информацией об ошибке, ожидая повторной попытки, что называется не блокирующий .
Стоит упомянуть, что вПараллельный ввод-выводПри обсуждении у нас также будет синхронная неблокирующая модель ввода-вывода, поскольку операции ввода-вывода (чтение/запись системных вызовов) фактически включают два этапа: инициирование запросов ввода-вывода и фактическое чтение и запись ввода-вывода. Разница между блокирующим вводом-выводом и неблокирующим вводом-выводом заключается в первом шаге, будет ли заблокирован процесс, инициирующий запрос ввода-вывода.Если он блокируется до тех пор, пока операция ввода-вывода не будет завершена и не вернется, это традиционный блокирующий ввод-вывод. не блокирует, это неблокирующий ввод-вывод. Разница между синхронным вводом-выводом и асинхронным вводом-выводом заключается во втором шаге: требуется ли фактическое чтение и запись ввода-вывода (копирование данных между режимом ядра и режимом пользователя) участия процесса, если требуется участие процесса, это синхронный ввод-вывод, если участие процесса не требуется, это асинхронный ввод-вывод. Если фактическое чтение и запись ввода-вывода требует участия запрашивающего процесса, то это синхронный ввод-вывод; поэтому блокирующий ввод-вывод, неблокирующий ввод-вывод, мультиплексирование ввода-вывода и ввод-вывод, управляемый сигналом, являются синхронным вводом-выводом.
Уровень параллелизма
В реальной среде развертывания, ограниченной количеством процессоров, невозможно увеличивать количество потоков до бесконечности, а требования параллелизма, требуемые разными сценариями, различны; например, в системе seckill мы делаем упор на высокий параллелизм и высокую пропускную способность. , в то время как для некоторых сервисов загрузки больше внимания уделяется быстрому отклику и малой задержке. Следовательно, мы также можем определить различные уровни параллелизма в соответствии с различными сценариями спроса:
-
Блокировка: Блокировка означает, что после того, как поток входит в критическую секцию, другие потоки должны ждать вне критической секции.После того, как входящий поток завершил свою задачу и покинул критическую секцию, другие потоки могут войти снова.
-
Отсутствие голодания: потоки ставятся в очередь в порядке очереди, независимо от размера приоритета, выполнение в порядке очереди не приводит к голоданию в ожидании ресурсов, т. выполняются в соответствии с приоритетом, который может стоять перед низкоприоритетным Поток с высоким приоритетом прерывается последующим потоком с высоким приоритетом, что приводит к голоданию
-
Безбарьерность: общие ресурсы не заблокированы, каждый поток может читать и записывать сам по себе, и если он изменяется другими потоками, он откатывает операцию и повторяет попытку до тех пор, пока отдельная операция не завершится успешно; риск заключается в том, что если несколько потоков найдут что они модифицировали друг друга, все потоки должны быть отброшены, что приведет к бесконечному циклу отката, что приведет к взаимоблокировке
-
Без блокировки: расширенная версия недоступна для блокировки, уровень блокировки отсутствует, чтобы гарантировать наличие хотя бы одного потока в ограниченных успешных процедурах выхода, независимо от того, была ли модификация успешной, что гарантирует, что откат нескольких потоков не приведет к бесконечному циклу.
-
Без ожидания: без ожидания — это обновленная версия без блокировки, наивысшего состояния параллельного программирования. Отсутствие блокировки гарантирует только успешное завершение потока, но есть потоки низкого уровня, которые всегда находятся в состоянии голодания. Без ожидания требуется, чтобы все потоки должны находиться в пределах ограниченного числа шагов.Завершите выход, дав низкоуровневым потокам шанс на выполнение, тем самым обеспечив выполнение всех потоков и улучшив параллелизм.
Количественная модель
Многопоточность не означает параллелизма, но параллелизм определенно является многопоточностью или многопроцессорностью; преимущество многопоточности заключается в том, что она может лучше использовать ресурсы и быстрее отвечать на запросы. Однако мы также знаем, что когда будет введена многопоточность, это приведет к увеличению сложности кодирования, а неправильное проектирование потоков приведет к увеличению затрат на переключение и накладных расходов на ресурсы. Как измерить повышение эффективности, вызванное многопоточностью, нам нужно измерить его с помощью двух законов.
Закон Амдала
Для расчета способности процессора повышать эффективность после параллельной работы можно использовать закон Амдала, предложенный Джином Амдалом в 1967 г. Он описывает в системе, основанной на соответствующих пропорциях параллелизуемых и сериализуемых компонентов, программу Сколько может теоретически можно ускорить за счет получения дополнительных вычислительных ресурсов. Любую программу или алгоритм можно разделить на параллелизуемые части в зависимости от того, можно ли их распараллелить или нет.1 - B
С частью B, которая не может быть распараллегирована, то согласно закону Amdahl, общее время выполнения программы с различными факторами параллелизации изменяется следующим образом:
Если F — это доля выполнения, которая должна быть сериализована, то закон Амдала говорит нам, что на машине с N-процессором мы можем ускориться не более чем:
Когда N увеличивается бесконечно и приближается к бесконечности, максимальное значение ускорения приближается к бесконечности1/F
, что означает, что если 50% обработки в программе нужно выполнять последовательно, ускорение можно увеличить только в 2 раза (независимо от того, сколько потоков реально доступно); если 10% программы нужно выполнять при последовательном выполнении ускорение может быть увеличено не более чем в 10 раз.
Закон Амдала также позволяет количественно оценить накладные расходы на сериализацию. В системе с 10 процессорами программы, сериализованные на 10 %, могут быть ускорены в 5,3 раза (использование 53 %), а в системе со 100 процессорами это число может достигать 9,2 (использование 9 %). Это делает невозможным достижение 10-кратного неэффективного использования ЦП. На рисунке ниже показана кривая максимальной загрузки процессора при последовательном выполнении и количестве процессоров. По мере увеличения числа процессоров становится очевидным, что даже небольшое процентное изменение степени сериализованного выполнения значительно ограничивает пропускную способность по мере увеличения вычислительных ресурсов.
Закон Амдала призван объяснить, что когда многоядерный ЦП оптимизирует систему, эффект оптимизации зависит от количества ЦП и доли сериализованных программ в системе; если только сосредоточиться на увеличении числа ЦП без уменьшения доли сериализации программ и не улучшает производительность системы.
Gustafson
Степень улучшения производительности системы, полученная за счет системной оптимизации компонента, зависит от того, как часто используется компонент, или в процентах от общего времени выполнения.
Модель памяти
Как описано ранее, современный компьютер обычно имеет два или более процессора, а также некоторые множества сердечников CPU; позволяет нескольким одновременным потокам, каждую нитью, в которой работает CPU во время среза. существуетУправление хранилищемВ этом разделе мы представили различные классы памяти в компьютерных системах:
Каждый ЦП содержит несколько регистров, которые по сути являются памятью ЦП; ЦП может выполнять операции в регистрах намного быстрее, чем в основной памяти. Каждый ЦП также может иметь уровень кэш-памяти ЦП, к которому ЦП может обращаться намного быстрее, чем к блоку основной памяти, но медленнее, чем к регистру. Компьютеры также включают основную память (ОЗУ), которая доступна для всех ЦП.Оперативная память обычно намного больше, чем кэш-память ЦП, но медленнее, чем кэш-память ЦП. Когда процессору необходимо получить доступ к основной памяти, он будет считывать часть данных из основной памяти в кэш-память ЦП и еще больше считывать часть данных из кеша во внутренний регистр, а затем работать с ним. Когда процессору необходимо записать данные в основную память, он записывает данные из регистра в кеш, а иногда сбрасывает данные из кеша в основную память. Будь то чтение или запись данных из кэша, нет необходимости читать или записывать все сразу, а работать только с частью данных.
Проблемы в параллельном программировании часто связаны с проблемами видимости, вызванными кэшированием, проблемами атомарности, вызванными переключением потоков, и проблемами упорядочения, вызванными оптимизацией компиляции. Взяв в качестве примера виртуальную машину Java, каждый поток имеет свой собственный стек потоков (стек вызовов), и по мере выполнения кода потока стек вызовов будет соответственно изменяться. Стек потоков содержит локальные переменные для каждого выполняемого метода. Каждый поток может обращаться только к своему стеку. Доступ к локальным переменным в стеке вызовов может получить только поток, создавший стек, и никакие другие потоки не могут получить к ним доступ. Даже если два потока выполняют один и тот же фрагмент кода, оба потока будут создавать локальные переменные в своих соответствующих стеках потоков. Поэтому у каждого потока есть свои локальные переменные. Все локальные переменные базовых типов хранятся в стеке потоков и не видны другим потокам. Поток может копировать базовые типы в другие потоки, но не может совместно использовать их с другими потоками, а объекты, созданные этим потоком, сохраняются в куче.
атомарность
Так называемая атомарность - это характеристика того, что одна или несколько операций не прерываются во время выполнения ЦП.Атомарные операции, гарантированные ЦП, находятся на уровне инструкций ЦП, а не операторов языков высокого уровня. Некоторые из наших, казалось бы, атомарных операций в языках программирования часто превращаются в множественные операции после компиляции в сборку:
i++
# 编译成汇编之后就是:
# 读取当前变量 i 并把它赋值给一个临时寄存器;
movl i(%rip), %eax
# 给临时寄存器+1;
addl $1, %eax
# 把 eax 的新值写回内存
movl %eax, i(%rip)
Мы ясно видим, что коду C требуется только одно предложение, но для его компиляции в сборку требуется три шага (оптимизация компилятора здесь не рассматривается, на самом деле три инструкции сборки можно объединить в одну с помощью оптимизации компилятора). Другими словами, только простое чтение и присваивание (и должны присваивать номер переменной, взаимное присваивание между переменными не является атомарной операцией) являются атомарными операциями. Проблема синхронизации решается методом атомарной операции: опираясь на поддержку примитивов процессора, три вышеуказанные инструкции объединяются в одну и выполняются как одна инструкция, чтобы гарантировать, что процесс выполнения не будет прерван, а многопоточный параллелизм не будет не беспокоить. Таким образом легко решается проблема синхронизации, которая представляет собой так называемую атомарную операцию. Однако процессор не обязан обеспечивать атомарные операции для любого фрагмента кода, тем более, что ресурсы нашей критической секции очень велики или даже неопределенного размера, процессору не нужно и не сложно обеспечивать атомарную поддержку. часто необходимо полагаться на блокировки для обеспечения атомарности.
Соответствующие атомарные операции/транзакции В Java операции чтения и присваивания переменных базовых типов данных являются атомарными операциями, то есть эти операции нельзя прервать, ни выполнить, ни нет. Модель памяти Java гарантирует только то, что базовые операции чтения и присваивания являются атомарными операциями.Если вы хотите добиться атомарности более широкого диапазона операций, вы можете использовать для этого синхронизацию и блокировку. Поскольку синхронизация и блокировка могут гарантировать, что только один поток выполняет блок кода в любой момент времени, проблема атомарности, естественно, отсутствует, что обеспечивает атомарность.
упорядоченность
Как следует из названия, упорядоченность относится к выполнению программы в том порядке, в котором выполняется код. Оптимизация кода и реорганизация инструкций компилятора в современных компиляторах могут повлиять на порядок выполнения кода. Перестановка инструкций во время компиляции предназначена для оптимизации доступа к переменным без изменения семантики кода путем корректировки порядка инструкций в коде. Тем самым максимально сокращается чтение и хранение регистров, а сами регистры полностью мультиплексируются. Однако компилятор может оценивать зависимости данных только в одном потоке выполнения и не может оценивать зависимости других потоков выполнения от конкурирующих данных. В качестве примера возьмем циклическую очередь без блокировок, если модуль записи сначала помещает данные, а затем обновляет индекс. Если индекс обновляется до данных, считыватель может прочитать грязные данные, так как считает, что индекс был обновлен.
Запретите компилятору оптимизировать этот тип переменных, решите проблему, заключающуюся в том, что переупорядочение во время компиляции не может гарантировать упорядочение, поскольку ЦП также имеет функцию выполнения вне порядка. Конвейерное выполнение и выполнение не по порядку — основные черты современных процессоров. Машинные инструкции проходят через такие операции, как выборка, декодирование, выполнение, выборка и обратная запись в конвейере. Для эффективности выполнения ЦП конвейеры обрабатываются параллельно, не влияя на семантику. Порядок процессора (Process Ordering, порядок, в котором машинные инструкции фактически выполняются ЦП) и порядок программы (Program Ordering, логический порядок выполнения программного кода) могут быть несогласованными, т. е. функция if-Serial удовлетворена. Очевидно, что незатронутая семантика здесь может гарантировать только явную причинность между инструкциями, но не может гарантировать неявную причинность. То есть нет никакой гарантии, что последовательности операций, которые семантически не связаны, но логически связаны с программой, выполняются по порядку. С тех пор функция самосогласования ЦП в одноядерную эпоху больше не существует в эпоху многоядерности, а многоядерный ЦП в целом больше не соответствует функции самосогласования.
Вкратце, если не предпринимаются дополнительные защитные меры, круговая очередь без блокировки в однозерной эпоху находится в многоядерном процессоре, а писатель на CPU Core записывает данные и обновляет индекс. То, как читатель на другом CPU Core полагается на этот индекс, чтобы определить, не обязательно ли данные не обязательно надежны. Индекс может быть написан перед данными, заставляя читателя прочитать грязные данные.
Классическая проблема, связанная с заказом в Java, - это режим Singleton. Например, мы будем использовать статическую функцию для получения экземпляра объекта и использовать синхронизированную блокировку, чтобы убедиться, что только один нить может вызвать создание, а другие потоки могут вызвать создание, а другие потоки могут напрямую получить его. В объект экземпляра.
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null){
instance = new Singleton();
}
}
}
Однако, хотя процесс создания объекта, который мы ожидаем, состоит из выделения памяти, инициализации объектов и присвоения ссылок на объекты переменным-членам, на практике оптимизированный код часто сначала присваивает переменные, а затем инициализирует объекты. Предполагая, что поток A сначала выполняет метод getInstance(), когда выполняется инструкция 2, происходит переключение потока и он переключается на поток B; если поток B в это время также выполняет метод getInstance(), то поток B выполняет первое суждение .найдетinstance != null
, поэтому экземпляр возвращается напрямую, и экземпляр в это время не был инициализирован.Если мы получим доступ к переменным-членам экземпляра в это время, может быть вызвано исключение нулевого указателя.
видимость
Так называемая видимость, то есть модификация разделяемой переменной потоком, которую сразу может увидеть другой поток. В эпоху одноядерных процессоров все потоки напрямую манипулируют данными одного ЦП, и запись в кэш потоком должна быть видна другому потоку; например, на следующем рисунке, если поток B обновляет значение переменной в thread A Доступ сделан, то необходимо получить самое последнее значение переменной V. В эпоху многоядерности каждый ЦП имеет собственный кеш, а общие переменные хранятся в основной памяти. Поток, работающий на ЦП, считывает общую переменную в свой собственный кэш ЦП. В кэше ЦП изменяется значение общего объекта, и, поскольку ЦП не сбрасывает данные из кэша обратно в основную память, изменение общей переменной невидимо для потока, работающего в другом ЦП. Таким образом, у каждого потока будет копия собственной общей переменной, которая хранится в соответствующем кэше процессора.
Процесс чтения и записи процессора
传统的 MESI 协议中有两个行为的执行成本比较大。一个是将某个 Cache Line 标记为 Invalid 状态,另一个是当某 Cache Line 当前状态为 Invalid 时写入新的数据。所以 CPU 通过 Store Buffer 和 Invalidate Queue 组件来降低这类操作的延时。 Как показано на рисунке:
Когда ядро выполняет запись в состоянии Invalid, оно сначала отправляет сообщение Invalid другим ядрам ЦП, а затем записывает текущие записанные данные в буфер хранения. Затем в какой-то момент асинхронно запишите в строку кэша. Если текущее ядро ЦП хочет прочитать данные в строке кэша, ему необходимо сначала просканировать буфер хранилища, а затем прочитать строку кэша (пересылка буфера хранилища). Однако в это время другие ядра ЦП не могут видеть данные в буфере хранения текущего ядра, и операция аннулирования не будет запущена до тех пор, пока данные в буфере хранения не будут сброшены в строку кэша. Когда ядро ЦП получает сообщение Invalid, оно записывает это сообщение в свою собственную очередь Invalidate Queue, а затем асинхронно устанавливает его в состояние Invalid. В отличие от Store Buffer, текущее ядро ЦП не сканирует часть Invalidate Queue при использовании Cache, поэтому может возникнуть очень краткосрочная проблема с грязным чтением. Конечно, термины Store Buffer и Invalidate Queue относятся к общей архитектуре SMP и не затрагивают конкретные архитектуры. На самом деле, в дополнение к Store Buffer и Load Buffer конвейер также содержит такие компоненты, как Line Fill Buffer/Write Combining Buffer для параллельной обработки.
Типичный случай: одновременное добавление
Самый классический случай проблемы видимости — параллельная операция сложения.Следующие два потока одновременно обновляют значение поля атрибута count переменной test.В первый раз count=0 будет прочитан в соответствующий ЦП caches, и выполнение будет завершено.count+=1
После этого значения в соответствующих кешах ЦП все равны 1, и после одновременной записи в память мы обнаружим, что память равна 1, а не 2, которые мы ожидали. После этого, поскольку соответствующие кэши ЦП имеют значение count, оба потока вычисляются на основе значения count в кэше ЦП, поэтому окончательное значение счетчика меньше 20000.
Thread th1 = new Thread(()->{
test.add10K();
});
Thread th2 = new Thread(()->{
test.add10K();
});
// 每个线程中对相同对象执行加操作
count += 1;
В Java, если несколько потоков совместно используют объект, а объявление volatile и синхронизация потоков не используются разумно, после того, как один поток обновит общий объект, другой поток не сможет получить последнее значение объекта. Когда общая переменная является энергозависимой, это гарантирует, что измененное значение будет немедленно обновлено в основной памяти, и когда другому потоку потребуется его прочитать, оно отправится в память для чтения нового значения. Видимость также может быть гарантирована с помощью synchronized и Lock, Synchronized и Lock могут гарантировать, что только один поток получает блокировку одновременно, а затем выполняет синхронизированный код, а изменения переменных сбрасываются в основную память перед снятием блокировки. Так что видимость гарантирована.
Строка кэша и ложное совместное использование | Строка кэша и ложное совместное использование
缓存系统中是以缓存行(Cache Line)为单位存储的,缓存行是 2 的整数幂个连续字节,一般为 32-256 个字节。 Наиболее распространенный размер строки кэша составляет 64 байта.当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
Если две переменные помещаются в одну и ту же строку кэша, в случае многопоточности они могут влиять на производительность друг друга. Как показано на рисунке выше, если поток на ЦП1 обновит переменную X, строка кэша на ЦП станет недействительной, а Y той же строки будет недействительной, даже если она не обновлена, что приведет к промаху кэша. Точно так же, если поток на CPU2 обновит Y, это приведет к тому, что строка кэша на CPU1 снова станет недействительной. Если ЦП часто пропускает кеш, пропускная способность системы упадет. Это проблема ложного обмена.
Чтобы решить проблему ложного разделения, вы можете занять определенную позицию заполнения до и после переменной и попытаться сделать так, чтобы переменная занимала полную строку кэша. Как показано на рисунке выше, поток на CPU1 обновляет X, так что Y на CPU2 не даст сбоев. Аналогично, если поток на ЦП2 обновляет Y, ЦП1 не выйдет из строя. Ссылаться наСхема памяти JavaКак видите, все объекты имеют заголовки длиной в два слова. Первое слово — это Mark Word, состоящий из 24-битного хэш-кода и 8-битных флаговых битов (например, состояния блокировки или объекта блокировки). Второе слово является ссылкой на класс, к которому принадлежит объект. В случае объекта массива требуется дополнительное слово для хранения длины массива. Начальный адрес каждого объекта выравнивается по 8 байтам для повышения производительности. Таким образом, для повышения эффективности при упаковке объектов порядок объявлений полей объекта переупорядочивается в следующем порядке на основе размера байта:
doubles (8) 和 longs (8)
ints (4) 和 floats (4)
shorts (2) 和 chars (2)
booleans (1) 和 bytes (1)
references (4/8)
<子类字段重复上述顺序>
Строка кэша имеет 64 байта, а заголовок объекта Java-программы имеет фиксированный размер 8 байт (32-разрядная система) или 12 байт (в 64-разрядной системе сжатие включено по умолчанию, а без сжатия оно равно 16 байтам). Нам просто нужно заполнить 6 бесполезных лонгов6*8=48
байт, чтобы разные объекты VolatileLong находились в разных строках кеша, можно избежать ложного совместного использования; не имеет значения, превышает ли 64-битная система 64 байта строки кеша, главное, чтобы разные потоки не работали на одном и том же строка кэша. Этот метод называется дополнением:
public final static class VolatileLong
{
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6; // 添加该行,错开缓存行,避免伪共享
}
Некоторые компиляторы Java оптимизируют неиспользуемые данные заполнения, то есть 6 длинных целых чисел в примере кода, во время компиляции.Вы можете добавить код в программу, чтобы предотвратить ее компиляцию и оптимизацию.
public static long preventFromOptimization(VolatileLong v) {
return v.p1 + v.p2 + v.p3 + v.p4 + v.p5 + v.p6;
}
барьер
Проблемы неупорядоченной оптимизации компилятора и неупорядоченной работы ЦП могут быть решены с помощью двух механизмов: барьера оптимизации и барьера памяти:
- Барьер оптимизации: позволяет избежать переупорядочения операций оптимизации компилятором и гарантирует, что инструкции перед барьером оптимизации не будут выполняться после барьера оптимизации при компиляции программы. Это гарантирует, что оптимизации времени компиляции не повлияют на фактический логический порядок кода.
- Барьер памяти делится на барьер записи (Store Barrier), барьер чтения (Load Barrier) и полный барьер (Full Barrier), которые выполняют две функции: предотвращают переупорядочение между инструкциями и обеспечивают видимость данных.
Несколько процессоров одновременно обращаются к общей основной памяти, и каждый процессор должен изменить порядок чтения и записи.После обновления данных их необходимо синхронно обновить в основной памяти (здесь не требуется обновлять основную память сразу после обновления кэша процессора). В этом случае перестановка кода и инструкций в сочетании с выводом инструкций, отложенных в кэше, изменяет порядок, в котором изменяются общие переменные, делая поведение программы непредсказуемым. Чтобы справиться с этим непредсказуемым поведением, процессор предоставляет набор машинных инструкций для обеспечения упорядоченности инструкций, что говорит процессору зафиксировать все невыполненные инструкции загрузки и сохранения перед продолжением выполнения. Также можно попросить компилятор не изменять порядок данной точки и окружающих последовательностей инструкций. Эти упорядоченные инструкции называются барьерами памяти. Конкретными гарантийными мерами на уровне языка программирования являются определение модели памяти.
POSIX, C++, Java имеют свою собственную модель разделяемой памяти и понимают, что разницы нет, но в некоторых деталях они немного отличаются. Модель памяти здесь не относится к структуре памяти, особенно памяти, кэш-памяти, процессору, буферу записи, командам чтения и записи для обеспечения средств защиты при работе с регистрами и другим аппаратным обеспечением, а также оптимизации компилятора интерактивного чтения и записи для обеспечения приказ. Эти сложные факторы можно обобщить двумя общими способами: перестановка и кэш, т. е. упомянутая выше перестановка кода, перестановка инструкций и кэш-память ЦП. Проще говоря, барьеры памяти делают две вещи:Отказаться от переупорядочивания, обновить кеш.
C++11 предоставляет набор пользовательских API std::memory_order для управления порядком чтения и записи процессора. Java использует правила «происходит до» для маскировки определенных гарантий, предписывая JVM чередовать барьерные инструкции во время генерации инструкций. Барьер памяти также может указывать во время компиляции, что инструкции или последовательности, включая окружающие инструкции, не оптимизированы.Он называется барьером компилятора и эквивалентен облегченному барьеру памяти.Его работа не менее важна, поскольку он направляет оптимизацию компилятора во время компиляции. Реализация барьеров немного сложнее, и мы используем абстрактный набор гипотетических инструкций для описания того, как работают барьеры памяти. Используйте MB_R, MB_W, MB, чтобы абстрагировать инструкции процессора в макросы:
- MB_R означает барьер чтения памяти, который гарантирует, что операции чтения не будут переупорядочены после вызова этой инструкции.
- MB_W означает барьер записи в память, который гарантирует, что операции записи не будут переупорядочены после вызова этой инструкции.
- MB означает барьер памяти считывания записи, что гарантирует, что предыдущие инструкции не переупорядочены после этого вызова этого инструкции.
Эти барьерные инструкции в равной степени эффективны на одном ядре процессора, хотя и не в качестве одного процессора для синхронизации данных между несколькими процессорами, но перестановка и кэш инструкций по-прежнему влияют на синхронизацию данных. Переупорядочивание инструкций очень низкоуровневое и позволяет достичь очень разных результатов, особенно уровень поддержки барьера памяти для разных архитектур, и даже то, что не поддерживает переупорядочение инструкций архитектуры, не должно использовать барьерную инструкцию. Конкретные инструкции о том, как использовать эти барьеры, реализованы на поддерживаемой платформе, компиляторе или виртуальной машине, нам нужно только использовать эту реализацию API (относится к различным одновременным ключам, блокировкам, повторному входу и т. д. в разделе подробности). . Цель здесь просто помочь лучше понять принцип барьера памяти.
Барьеры памяти имеют большое значение и являются ключом к обеспечению надлежащего параллелизма. Правильно установив барьеры памяти, мы можем гарантировать, что инструкции будут выполняться в том порядке, в котором мы ожидаем. Предупреждение здесь заключается в том, что маски памяти следует применять только к инструкциям, которые необходимо синхронизировать или которые также могут содержать фрагменты окружающих инструкций. Большинство современных архитектур процессоров бессмысленны, если они используются для синхронизации всех инструкций.
дальнейшее чтение
Вы можете прочитать серию статей автора в Gitbook с помощью следующей навигации, охватывающей ввод технических данных, язык и теорию программирования, веб-интерфейс и большой интерфейс, разработку и инфраструктуру на стороне сервера, облачные вычисления и большие данные, науку о данных и искусственный интеллект. , Дизайн продукта и другие области:
-
Система знаний:"Удивительные списки | Сбор данных CS", "Потрясающие шпаргалки | Шпаргалка по быстрому обучению", "Потрясающие интервью | Основы собеседования при приеме на работу", "Потрясающие дорожные карты | Руководство для начинающих программистов", "Потрясающие карты разума | Карта разума в контексте знаний", "Awesome-CS-Books Коллекция книг с открытым исходным кодом (.pdf)》
-
Язык программирования:"теория языка программирования", "Ява в действии", "JavaScript в действии", "Перейти в действие", "Питон в действии", "Ржавчина в действии》
-
Программная инженерия, шаблоны и архитектура: "Парадигмы программирования и шаблоны проектирования", "Структуры данных и алгоритмы", "проектирование архитектуры программного обеспечения", "Очистить и рефакторить", "Методы и инструменты НИОКР》
-
Интернет и большой интерфейс: 《Современные основы веб-разработки и инженерные практики", "визуализация данных", "iOS", "Android", "Гибридная разработка и кросс-энд приложение》
-
Практика разработки серверов и инженерная архитектура: 《Серверная база", "Микросервисы и Cloud Native", "Гарантия тестирования и высокой доступности", "DevOps", "Node", "Spring", "Информационная безопасность и тестирование на проникновение》
-
Распределенная инфраструктура: "Распределенные системы", "Распределенных вычислений", "база данных", "Интернет", "Виртуализация и оркестровка", "Облачные вычисления и большие данные", "Linux и операционные системы》
-
Наука о данных, искусственный интеллект и глубокое обучение: "Математическая статистика", "анализ данных", "машинное обучение", "глубокое обучение", "обработка естественного языка", "Инструменты и техника", "Промышленное применение》
-
Дизайн продукта и взаимодействие с пользователем: 《дизайн продукта", "Интерактивный опыт", "управление проектом》
-
Применение в отрасли: "Мифы индустрии", "функциональный домен", "электронная коммерция", "Умное производство》
Кроме того, вы можете перейти кxCompassИнтерактивный поиск и найдите нужную статью/ссылку/книгу/курс; илиМАТРИЦА Матрица артикулов и индексов кодовСм. статьи и исходный код проекта для получения более подробной информации о навигации по каталогам в файлах . Наконец, вы также можете следить за публичной учетной записью WeChat: "Медвежья технологическая дорога' для последней информации.