, чтобы убедиться, что изменения в счетчике не могут быть прерваны другим потоком.
Это очень неудачная формулировка, и она неточна. На самом деле прерывания - это наименьшая из наших забот.
Что касается параллелизма, модель памяти Rust основана на модели памяти, принятой в C11 и C ++ 11. Если вы хотите узнать больше о моделях памяти, я рекомендую прочитать только статью Preshing о Слабые и сильные модели памяти ; Я постараюсь передать материальную справедливость в этом ответе.
Что такое модель памяти, спросите вы?
Грубо говоря, модель памяти - это модель, которая определяет, какие операции могут быть переупорядочены , и что не может.
Может произойти переупорядочение:
В общем по соображениям производительности переупорядочение операций чтения / записи: хорошо . Это позволяет более эффективно использовать ЦП и ускорить прогресс. Однако правильность некоторых алгоритмов зависит от различных потоков, наблюдающих за событиями в определенном порядке ... и поэтому иногда некоторые операции чтения / записи не следует переупорядочивать. Модель памяти и порядок памяти используются для определения точных ограничений, которые компилятор и ЦП должны соблюдать для правильного выполнения алгоритма.
Каким образом ЦП может выйти из строя Rc
?
Путем игнорирования приращений.
В модели со слабой памятью, если два разных ядра увеличивают счетчик, одно из приращений может быть проигнорировано.
Представьте себе следующую временную шкалу в данном потоке, где CN означает, что текущее количество владельцев - N, а C0 подразумевает уничтожение.
T1 -- Create: C1 --- Clone: C2 -- Drop Clone: C1 --- Drop: C0.
Теперь представьте, что этот поток разделяет Rc
:
T1 -- Create: C1 --- Clone: C2 ---------------C1---- Drop Clone: C0 --- Access **BOOM**.
\ /
T2 \_ Clone: C2 -- Drop Clone: C1 _/
^ ^
Only one increment was seen But both decrements are
Зачем ЦП это делает ?
Производительность.
Сильная модель памяти означает много ненужного трепа между ядрами для синхронизации строк кэша - треп, который увеличивает задержку операций.
Более слабые модели памяти позволяют меньше болтовни и, следовательно, меньше задержек, что означает, что программы могут выполняться быстрее или с меньшим энергопотреблением.
А если модель памяти достаточно сильна?
Даже на гипотетическом процессоре, где каждое чтение / запись касается памяти, это может быть go ошибочным из-за условий гонки.
В частности:
- T1 читает count (1), T1 вычисляет увеличенный счетчик 2, T1 записывает счетчик (2).
- T2 считывает счетчик (1), T2 вычисляет увеличенный счетчик 2, T2 записывает счетчик (2).
Если вы посмотрите на типы AtomicXXX
в Rust, вы заметите наличие ряда операций RMW (чтение-изменение-запись), таких как fetch_add
, которые атомарно считывают, увеличивают и записывают.
Атомарность важна, иначе могут возникнуть условия гонки.
Каким образом оптимизатор может сорваться Rc
?
Даже на гипотетическом ЦП без каких-либо регистров, где приращение / декремент напрямую изменит память атомарно, все еще может go неправильно.
Оптимизатору разрешено предполагать, что никакой другой поток выполнения не наблюдает за записью в память при отсутствии упорядочения памяти: это будет Undefine d В конце концов, поведение.
Следовательно, оптимизатору будет вполне разрешено:
- Создать клон
Rc
. - Отбросить оригинал.
- Уменьшить счетчик (-2) - объединенные уменьшения для развлечения и прибыли!
- Используйте клон.
- Увеличьте счетчик (+1).
- Отбросьте клон.
Если другой поток отбрасывает последнюю ссылку между (3) и (5), счетчик достигнет 0, поэтому другой поток сбросит значение внутри.
Я не уверен, что понимаю ...
Не волнуйтесь, у вас нет to!
Компилятор Rust за вами. Если вы не выберете unsafe
, это гарантирует, что вы случайно не введете такие условия гонки.
Что касается понимания всего этого, существует множество литературы. Точные эффекты упорядочивания задокументированы , и для большей картины Preshing действительно хорош, я от всей души рекомендую их блог.