Почему именно поток Rc небезопасен? - PullRequest
2 голосов
/ 20 июня 2020

Я читаю главу 16 - Параллелизм общего состояния из Язык программирования Rust . Он говорит:

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

Что означает

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

Единственный сценарий, в котором, как мне кажется, изменения для подсчета прерываются, - это если поток создается, и каким-то образом он паникует / вылетает , поэтому шкафчик никогда не разблокируется, поэтому количество ссылок никогда не уменьшается. Я могу представить Rust, вызывающий деструкторы каждого объекта в области видимости, если произойдет пани c.

Может кто-нибудь прояснить это для меня?

1 Ответ

10 голосов
/ 20 июня 2020

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

Это очень неудачная формулировка, и она неточна. На самом деле прерывания - это наименьшая из наших забот.

Что касается параллелизма, модель памяти 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 В конце концов, поведение.

Следовательно, оптимизатору будет вполне разрешено:

  1. Создать клон Rc.
  2. Отбросить оригинал.
  3. Уменьшить счетчик (-2) - объединенные уменьшения для развлечения и прибыли!
  4. Используйте клон.
  5. Увеличьте счетчик (+1).
  6. Отбросьте клон.

Если другой поток отбрасывает последнюю ссылку между (3) и (5), счетчик достигнет 0, поэтому другой поток сбросит значение внутри.

Я не уверен, что понимаю ...

Не волнуйтесь, у вас нет to!

Компилятор Rust за вами. Если вы не выберете unsafe, это гарантирует, что вы случайно не введете такие условия гонки.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...