Alexander 2 лет назад
Родитель
Сommit
0f0d8bdfbf
1 измененных файлов с 2635 добавлено и 0 удалено
  1. 2635 0
      13-параллельные-вычисления/README.md

+ 2635 - 0
13-параллельные-вычисления/README.md

@@ -0,0 +1,2635 @@
+# 13. Параллельные вычисления
+
+- [13.1. Революция в области параллельных вычислений]()
+- [13.2. Краткая история механизмов разделения данных]()
+- [13.3. Смотри, мам, никакого разделения (по умолчанию)]()
+- [13.4. Запускаем поток]()
+    - [13.4.1. Неизменяемое разделение]()
+- [13.5. Обмен сообщениями между потоками]()
+- [13.6. Сопоставление по шаблону с помощью receive]()
+    - [13.6.1. Первое совпадение]()
+    - [13.6.2. Соответствие любому сообщению]()
+- [13.7. Копирование файлов – с выкрутасом]()
+- [13.8. Останов потока]()
+- [13.9. Передача нештатных сообщений]()
+- [13.10. Переполнение почтового ящика]()
+- [13.11. Квалификатор типа shared]()
+    - [13.11.1. Сюжет усложняется: квалификатор shared транзитивен]()
+- [13.12. Операции с разделяемыми данными и их применение]()
+    - [13.12.1. Последовательная целостность разделяемых данных]()
+- [13.13. Синхронизация на основе блокировок через синхронизированные классы]()
+- [13.14. Типизация полей в синхронизированных классах]()
+    - [13.14.1. Временная защита == нет утечкам]()
+    - [13.14.2. Локальная защита == разделение хвостов]()
+    - [13.14.3. Принудительные идентичные мьютексы]()
+    - [13.14.4. Фильм ужасов: приведение от shared]()
+- [13.15. Взаимоблокировки и инструкция synchronized]()
+- [13.16. Кодирование без блокировок с помощью разделяемых классов]()
+    - [13.16.1. Разделяемые классы]()
+    - [13.16.2. Пара структур без блокировок]()
+- [13.17. Статические конструкторы и потоки]()
+- [13.18. Итоги]()
+
+Благодаря сложившейся обстановке в индустрии аппаратного обеспече
+ния качественно изменился способ доступа к вычислительным ресур
+сам, которые, в свою очередь, требуют основательного пересмотра тех
+ники вычислений и применяемых языковых абстракций. Сегодня ши
+роко распространены параллельные вычисления, и программное обес
+печение должно научиться извлекать из этого пользу.
+
+Несмотря на то что индустрия программного обеспечения в целом еще
+не выработала окончательные ответы на вопросы, поставленные рево
+люцией в области параллельных вычислений, молодость D позволила
+его создателям, не связанным ни устаревшими концепциями прошло
+го, ни огромным наследством базового кода, принять компетентные ре
+шения относительно параллелизма. Главное отличие подхода D от стан
+дарта поддерживающих параллелизм императивных языков – в том,
+что он не поощряет разделение данных между потоками; по умолчанию
+параллельные потоки фактически изолированы друг от друга с помо
+щью механизмов языка. Разделение данных разрешено, но лишь в огра
+ниченной управляемой форме, чтобы компилятор мог предоставлять
+основательные глобальные гарантии.
+
+В то же время D, оставаясь в душе языком для системного программи
+рования, разрешает применять ряд низкоуровневых, неконтролируе
+мых механизмов параллельных вычислений. (При этом в безопасных
+программах некоторые из этих механизмов использовать запрещено.)
+
+Вот краткий обзор уровней параллелизма, предлагаемых языком D:
+
+- Передовой подход к параллельным вычислениям заключается в ис
+пользовании изолированных потоков или процессов, взаимодейст
+вующих с помощью сообщений. Эта парадигма, называемая *обменом сообщениями* (*message passing*), позволяет создавать безопасные
+модульные программы, легкие для понимания и сопровождения.
+Обмен сообщениями успешно применяется в разнообразных языках
+и библиотеках. Раньше обмен сообщениями был медленнее подходов,
+основанных на разделении памяти, поэтому он и не стал общеприня
+тым, но за последнее время здесь многое бесповоротно изменилось.
+Параллельные программы на D используют обмен сообщениями –
+парадигму, ориентированную на всестороннюю инфраструктурную
+поддержку.
+- D также поддерживает старомодную синхронизацию на основе кри
+тических участков, защищенных мьютексами и флагами событий.
+В последнее время этот подход к организации параллельных вычис
+лений подвергается серьезной критике за недостаточную масшта
+бируемость для настоящих и будущих параллельных архитектур.
+D строго управляет разделением данных, ограничивая возможности
+программирования с применением блокировок. На первый взгляд
+это ограничение может показаться суровым, но оно избавляет осно
+ванный на блокировках код от его злейшего врага – низкоуровневых
+гонок за данными (ситуаций состязания). При этом разделение дан
+ных остается наиболее эффективным средством передачи больших
+объемов данных между потоками, так что пренебрегать им не стоит.
+- По традиции языков системного уровня программы на D, не имею
+щие атрибута `@safe`, могут посредством приведений достигать бес
+препятственного разделения данных. За корректность таких про
+грамм в основном отвечаете вы.
+- Если вам мало предыдущего уровня, конструкция `asm` позволяет по
+лучить полный контроль над машинными ресурсами. Для еще бо
+лее низкоуровневого контроля потребуются микропаяльник и очень,
+очень верная рука.
+
+Прежде чем с головой окунуться во все это, отвлечемся ненадолго, что
+бы поближе присмотреться к тем аппаратным усовершенствованиям,
+которые потрясли мир.
+
+[В начало ⮍](#13-параллельные-вычисления)
+
+## 13.1. Революция в области параллельных вычислений
+
+Что касается параллельных вычислений, то для них сейчас времена по
+интереснее, чем когда-либо. Это времена, когда и хорошие, и плохие но
+вости вписываются в общую панораму компромиссов, противоборств
+и тенденций.
+
+Хорошие новости в том, что степень интеграции все еще растет по зако
+ну Мура[^1]; судя по тому, что нам уже известно, и по тому, что мы сегодня
+можем предположить, это продлится как минимум лет десять после
+выхода этой книги. Курс на миниатюризацию означает рост плотности
+вычислительной мощности пропорционально числу совместно работаю
+щих транзисторов на единицу площади. Все ближе друг к другу компо
+ненты, все короче соединения, а это означает повышение скорости ло
+кальной связности – золотое дно в плане быстродействия.
+
+К сожалению, отдельные выводы, начинающиеся со слов «к сожале
+нию», умеряют энтузиазм по поводу возросшей вычислительной плот
+ности. Во-первых, существует не только локальная связность – она фор
+мируется в иерархию: тесно связанные компоненты образуют бло
+ки, которые должны связываться с другими блоками, образуя блоки
+большего размера. В свою очередь, блоки большего размера также со
+единяются с другими блоками большего размера, образуя функцио
+нальные блоки еще большего размера, и т. д. На своем уровне связности
+такие блоки остаются «далеки» друг от друга. Хуже того, возросшая
+сложность каждого блока увеличивает сложность связей между блока
+ми, что реализуется путем уменьшения толщины проводов и расстоя
+ния между ними. Это означает рост сопротивления, электроемкости
+и перекрестных помех. Перекрестные помехи – это способность сигнала
+из одного провода распространяться на соседние провода посредством
+(в данном случае) электромагнитного поля. На высоких частотах про
+вод – практически антенна, и помехи становятся настолько невыноси
+мыми, что сегодня параллельные соединения все чаще заменяют после
+довательными (своего рода феномен нелогичности, заметный на всех
+уровнях: USB заменил параллельный порт, в качестве интерфейса на
+копителей данных SATA заменил PATA, а в подсистемах памяти после
+довательные шины заменяют параллельные, и все из-за перекрестных
+помех. Где те золотые деньки, когда параллельное было быстрее, а по
+следовательное медленнее?).
+
+Кроме того, растет разрыв в производительности между вычислитель
+ными элементами и памятью. В то время как плотность памяти, как
+и ожидалось, увеличивается в соответствии с общей степенью интегра
+ции, скорость доступа к ней все больше отстает от скорости вычислений
+из-за множества разнообразных физических, технологических и ры
+ночных факторов. В настоящее время неясно, что поможет сущест
+венно сократить этот разрыв в быстродействии, и он лишь растет. Тыся
+чи тактов могут отделять процессор от слова в памяти; а ведь еще не
+сколько лет назад можно было купить микросхемы памяти «с нулевым
+временем ожидания», обращение к которым осуществлялось за один
+такт.
+
+Из-за широкого спектра архитектур памяти, представляющих собой
+различные компромиссные решения относительно плотности, цены
+и скорости, повысилась и изощренность иерархий памяти; обращение
+к единственному слову памяти превратилось в детективное расследова
+ние с опросом нескольких уровней кэша, начиная с драгоценного стати
+ческого ОЗУ прямо на микросхеме и порой проходя весь путь до массо
+вой памяти. Возможна и противоположная ситуация: копии указан
+ных данных могут располагаться во множестве мест по всей иерархии.
+Это, в свою очередь, тоже влияет на модели программирования. Мы
+больше не можем позволить себе представлять память большим моно
+литом, удобным для разделения всеми процессами системы: наличие
+кэшей провоцирует рост локального трафика в памяти, превращая раз
+деляемые данные в иллюзию, которую все труднее сопровождать.
+
+К последним сенсационным известиям относится то, что скорость света
+упрямо решила оставаться неизменной (`immutable`, если хотите) – около
+300 000 000 метров в секунду. Скорость же света в оксиде кремния (соот
+ветствующая скорости распространения сигнала внутри современных
+микросхем) составляет примерно половину этого значения, причем дос
+тижимая сегодня скорость переноса самих данных существенно ниже
+этого теоретического предела. Это означает больше проблем с глобаль
+ной взаимосвязанностью на высоких частотах. Если бы у нас была мик
+росхема с частотой 10 ГГц, то простое перемещение бита с одного на
+другой конец этого чипа шириной 4,5 см (по сути, вообще без вычисле
+ний) в идеальных условиях занимало бы три такта.
+
+Словом, наступает век процессоров очень высокой плотности и гигант
+ской вычислительной мощности, при этом все более изолированных
+и труднодоступных, которые сложно использовать из-за ограничений
+взаимосвязности, скорости распространения сигнала и быстроты до
+ступа к памяти.
+
+Компьютерная индустрия, естественно, обходит эти преграды. Одним
+из феноменов стало резкое сокращение размеров и энергии, требуемых
+для заданной вычислительной мощности; всего лишь пять лет назад
+уровень технологии не позволял достичь компактности и возможно
+стей КПК, без которых сегодня мы как без рук. При этом традицион
+ные компьютеры, пытающиеся повысить вычислительную мощность
+при тех же размерах, представляют все меньший интерес. Производи
+тели микросхем для них уже не борются за повышение тактовой часто
+ты, предлагая взамен вычислительную мощность в уже знакомой упа
+ковке: несколько одинаковых центральных процессоров, соединенных
+шинами друг с другом и с памятью. Так что спустя каких-то несколько
+лет отвечать за разгон компьютеров будут не электронщики, а в основ
+ном программисты. Вариант «побольше процессоров» может показать
+ся довольно заманчивым, но типовым задачам настольного компьютера
+не под силу эффективно использовать и восемь процессоров. В будущем
+предполагается экспоненциальный рост числа доступных процессоров
+до десятков, сотен и тысяч. При разгоне единственной программы про
+граммистам придется очень много потрудиться, чтобы продуктивно ис
+пользовать *все* эти процессоры.
+
+Из-за разных технологических и человеческих факторов в компьютер
+ной индустрии постоянно случаются подвижки и сотрясения, но на
+этот раз мы, кажется, дошли до точки. С недавних пор взять отпуск,
+чтобы увеличить скорость работы программы, – уже не вариант. Это
+возмутительно. Это подрыв устоев. Это революция в области парал
+лельных вычислений.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.2. Краткая история механизмов разделения данных
+
+Один из аспектов перемен в компьютерной индустрии – внезапность,
+с какой сегодня меняются модели обработки данных и параллелизма,
+особенно на фоне темпа развития языков и парадигм программирова
+ния. Чтобы язык и связанные с ним стили отпечатались в сознании со
+общества программистов, нужны годы, даже десятки лет, а в области
+параллелизма начиная с 2000-х все меняется в геометрической про
+грессии.
+
+Например, наше прошлогоднее понимание основ параллелизма[^2] тяготе
+ло к разделению данных, порожденному мейнфреймами 1960-х. Тогда
+процессорное время было настолько дорогим, что повысить общую эф
+фективность использования процессора можно было, только разделяя
+его между множеством программ, управляемых множеством операто
+ров. *Процесс* определялся и определяется как совокупность состояния
+и ресурсов исполняющейся программы. Процессор (центральное про
+цессорное устройство, ЦПУ) реализует разделение времени с помощью
+планировщика задач и прерываний таймера. По каждому прерыванию
+таймера планировщик решает, какому процессу предоставить ЦПУ на
+следующий квант времени, создавая таким образом иллюзию одновре
+менного исполнения нескольких процессов, хотя на самом деле все они
+используют одно и то же ЦПУ.
+
+Чтобы ошибочные процессы не повредили друг другу и коду операцион
+ной системы, была введена *аппаратная защита памяти*. Для надеж
+ной изоляции процессов в современных системах защиту памяти соче
+тают с *виртуализацией памяти*: каждый процесс считает память ма
+шины «своей собственностью», хотя на самом деле все взаимодействие
+между процессом и памятью, а также изоляцию процессов друг от дру
+га берет на себя уровень-посредник, транслирующий логические адреса
+(так видит память процесс) в физические (так обращается к памяти ма
+шина). Хорошие новости в том, что процессы, вышедшие из-под конт
+роля, могут навредить только себе, но не другим процессам и не ядру
+операционной системы. Новости похуже в том, что каждое переключе
+ние задач требует потенциально дорогой смены адресных пространств
+процессов, не говоря о том, что каждый процесс при переключении на
+него «просыпается» с амнезией кэша, поскольку глобальный кэш обыч
+но используется всеми процессами. Так и появились *потоки* (*threads*).
+Поток – это процесс, не владеющий информацией о том, как трансли
+ровать адреса; это чистый контекст исполнения: состояние процессора
+плюс стек. Несколько потоков разделяют адресное пространство про
+цесса, то есть порождать потоки и переключаться между ними относи
+тельно дешево, и они могут с легкостью и без особых затрат разделять
+данные друг с другом. Разделение памяти между потоками, запущен
+ными на одном ЦПУ, осуществляется настолько прямолинейно, на
+сколько это возможно: один поток пишет, другой читает. При использо
+вании техники разделения времени порядок записи данных, естествен
+но, совпадает с порядком, в котором эти записи будут видны другим
+потокам. Поддержку более высокоуровневых инвариантов данных
+обеспечивают механизмы блокировки, например критические секции,
+защищенные с помощью примитивов синхронизации (таких как сема
+форы и мьютексы). В последние годы XX века то, что можно назвать
+«классическим» многопоточным программированием (которое харак
+теризуется разделяемым адресным пространством, простыми правила
+ми видимости изменений и синхронизацией на мьютексах), обросло
+массой наблюдений, народных мудростей и анекдотов. Существовали
+и другие модели организации параллельных вычислений, но на боль
+шинстве машин применялась классическая многопоточность.
+
+Основные императивные языки наших дней (такие как C, C++, Java)
+развивались в век классической многопоточности – в старые добрые
+времена простых архитектур памяти, понятных примитивов взаимо
+блокировки и разделения данных без изысков. Языки, естественно, мо
+делировали реалии аппаратного обеспечения того времени (когда подра
+зумевалось, что потоки разделяют одну и ту же область памяти) и вклю
+чали соответствующие средства. В конце концов само определение мно
+гопоточности подразумевает, что все потоки, в отличие от процессов
+операционной системы, разделяют одно общее адресное пространство.
+Кроме того, API для реализации обмена сообщениями (например, спе
+цификация MPI [29]) были доступны лишь в форме библиотек, изна
+чально созданных для специализированного дорогостоящего аппарат
+ного обеспечения, такого как кластеры (супер)компьютеров.
+
+Тогда еще только зарождающиеся функциональные языки заняли прин
+ципиальную позицию, основанную на математической чистоте: «Мы не
+заинтересованы в моделировании аппаратного обеспечения, – сказали
+они. – Нам хотелось бы моделировать математику». А в математике ред
+ко что-то меняется, математические результаты инвариантны во време
+ни, что делает математические вычисления идеальным кандидатом для
+распараллеливания. (Только представьте, как первые программисты –
+вчерашние математики, услышав о параллельных вычислениях, чешут
+затылки, восклицая: «Минуточку!..») Функциональные программисты
+убеждены, что такая модель вычислений поощряет неупорядоченное,
+параллельное выполнение, однако до недавнего времени эта возможно
+сть являлась скорее потенциальной энергией, чем достигнутой целью.
+Наконец был разработан язык Erlang. Он начал свой путь в конце 1980-х
+как предметно-ориентированный встроенный язык приложений для
+телефонии. Предметная область, предполагая десятки тысяч программ,
+одновременно запущенных на одной машине, заставляла отдать пред
+почтение обмену сообщениями, когда информация передается в стиле
+«выстрелил – забыл». Аппаратное обеспечение и операционные систе
+мы по большей части не были оптимизированы для таких нагрузок, но
+Erlang изначально запускался на специализированной платформе. В ре
+зультате получился язык, оригинальным образом сочетающий нечис
+тый функциональный стиль, серьезные возможности для параллель
+ных вычислений и стойкое предпочтение обмена сообщениями (ника
+кого разделения памяти!).
+
+Перенесемся в 2010-е. Сегодня даже у средних машин больше одного
+процессора, а главная задача десятилетия – уместить на кристалле как
+можно больше ЦПУ. Отсюда и последствия, самое важное из которых –
+конец монолитной разделяемой памяти.
+
+С одним разделяемым по времени ЦПУ связана одна подсистема памя
+ти – с буферами, несколькими уровнями кэшей, все по полной програм
+ме. Независимо от того, как ЦПУ управляет разделением времени, чте
+ние и запись проходят по одному и тому же маршруту, а потому видение
+памяти у разных потоков остается когерентным. Несколько взаимосвя
+занных ЦПУ, напротив, не могут позволить себе разделять подсистему
+кэша: такой кэш потребовал бы мультипортового доступа (что дорого
+и слабо масштабируемо), и его было бы трудно разместить в непосредст
+венной близости ко всем ЦПУ сразу. Вот почему практически все совре
+менные ЦПУ производятся со своей кэш-памятью, предназначенной
+лишь для их собственных нужд. Производительность мультипроцес
+сорной системы зависит главным образом от аппаратного обеспечения
+и протоколов, соединяющих комплексы ЦПУ+кэш.
+
+Несколько кэшей превращают разделение данных между потоками
+в чертовски сложную задачу. Теперь операции чтения и записи в раз
+ных потоках могут затрагивать разные кэши, поэтому сделать так, что
+бы один поток делился данными с другим, стало сложнее, чем раньше.
+На самом деле, этот процесс превращается в своего рода обмен сообще
+ниями[^3]: в каждом случае такого разделения между подсистемами кэ
+шей должно иметь место что-то вроде рукопожатия, обеспечивающего
+попадание разделяемых данных от последнего записавшего потока к чи
+тающему потоку, а также в основную память.
+
+Протоколы синхронизации кэшей добавляют к сюжету еще один пово
+рот (хотя и без него все было достаточно лихо закручено): они восприни
+мают данные только блоками, не предусматривая чтение и запись от
+дельных слов. То есть общающиеся друг с другом процессы «не помнят»
+точный порядок, в котором записывались данные, что приводит к пара
+доксальному поведению, которое не поддается разумному объяснению
+и противоречит здравому смыслу: один поток записывает x, а затем y,
+и в некоторый промежуток времени другой поток видит новое y, но ста
+рое x. Такие нарушения причинно-следственных связей слабо вписыва
+ются в общую модель классической многопоточности. Даже наиболее
+сведущим в классической многопоточности программистам невероятно
+трудно адаптировать свой стиль и шаблоны программирования к но
+вым архитектурам памяти.
+
+Проиллюстрируем скоростные изменения в современных параллель
+ных вычислениях и серьезное влияние разделения данных на подходы
+языков к параллелизму советом из чудесной книги «Java. Эффективное
+программирование» издания 2001 года [8, разд. 51, с. 204]:
+
+> «Если есть несколько готовых к исполнению потоков, планировщик пото
+ков определит, какие потоки должны запуститься и на какое время… Луч
+ший способ написать отказоустойчивое, оперативное и переносимое прило
+жение – стараться иметь минимум готовых к исполнению потоков в любой
+момент времени.»
+
+Сегодняшний читатель сразу же отметит поразительную деталь: здесь
+не просто говорится об однопроцессорном программировании с много
+поточностью на основе разделения времени, но подразумевается един
+ственность процессора, хоть и без явной констатации. Естественно, что
+в издании 2008 года[^4] этот совет был изменен на «стремиться к тому,
+чтобы среднее число готовых к исполнению потоков было ненамного
+больше числа процессоров». Любопытно, что даже этот совет, на вид ра
+зумный, подразумевает два невысказанных допущения: 1) за счет дан
+ных потоки будут сильно связаны друг с другом, что в свою очередь
+приведет к снижению быстродействия из-за накладных расходов на
+взаимоблокировки, и 2) число процессоров на машинах, где может за
+пускаться программа, примерно одинаково. И тогда этот совет полно
+стью противоположен тому, что настойчиво повторяется в книге «Про
+граммирование на языке Erlang» [5, глава 20, с. 363]:
+
+> «**Используйте много процессоров**. Это важно: мы должны держать свои
+ЦПУ в занятом состоянии. Все ЦПУ должны быть заняты в каждый момент
+времени. Легче всего достигнуть этого, имея много процессов[^5]. Говоря „мно
+го процессов“, я имею в виду много по отношению к количеству ЦПУ. Если
+у нас много процессов, то о занятом состоянии для ЦПУ можно не беспоко
+иться.»
+
+Какой из этих трех рекомендаций следовать? Как обычно, все зависит от
+обстоятельств. Первая прекрасно подходит для аппаратного обеспечения
+2001 года; вторая – для сценариев, характеризующихся интенсивной
+работой с разделяемыми данными и, следовательно, жестким соперни
+чеством; третья полезна в условиях слабого соперничества и большого
+количества ЦПУ.
+
+Поддерживать разделение памяти все сложнее, этот подход к организа
+ции параллельных вычислений начинает казаться неубедительным,
+в моду входят функциональность и обмен сообщениями. Неудивитель
+но, что в последние годы растет интерес к Erlang и другим функцио
+нальным языкам, удобным для разработки приложений с параллель
+ными вычислениями.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.3. Смотри, мам, никакого разделения (по умолчанию)
+
+Вследствие последних усовершенствований аппаратного и программ
+ного обеспечения D решил отойти от других императивных языков: да,
+язык D поддерживает потоки, но они не разделяют никакие изменяе
+мые данные по умолчанию – они изолированы друг от друга. Изоляция
+обеспечивается не аппаратно (как в случае с процессами) и не с помо
+щью проверок времени исполнения; она является естественным следст
+вием устройства системы типов D.
+
+Это решение в духе функциональных языков, которые также старают
+ся запретить любые изменения, а значит, и разделение изменяемых
+данных. Но есть два различия. Во-первых, программы на D все же мо
+гут свободно использовать изменяемые данные – закрыта лишь воз
+можность непреднамеренного обращения к изменяемым данным для
+других потоков. Во-вторых, «никакого разделения» – это лишь выбор
+*по умолчанию*, но не *единственный* возможный. Чтобы определить дан
+ные как разделяемые между потоками, необходимо уточнить их опре
+деление с помощью ключевого слова `shared`. Рассмотрим пример двух
+простых определений, размещенных в корне модуля:
+
+```d
+int perThread;
+shared int perProcess;
+```
+
+В большинстве языков первое определение (или его синтаксический эк
+вивалент) означало бы ввод глобальной переменной, используемой все
+ми потоками, но в D у переменной `perThread` есть отдельная копия для
+каждого потока. Второе определение выделяет память лишь под одно
+значение типа `int`, разделяемое всеми потоками, так что в некотором ро
+де оно ближе (но не идентично) к традиционной глобальной переменной.
+
+Переменная `perThread` сохраняется при помощи средства операционной
+системы, называемого локальным хранилищем потока (thread-local
+storage, TLS). Скорость доступа к данным, память под которые выделе
+на в TLS, зависит от реализации компилятора и базовой операционной
+системы. В общем случае эта скорость лишь незначительно меньше,
+скажем, скорости обращения к обычной глобальной переменной в про
+грамме на C. В редких случаях, когда эта разница может иметь значе
+ние, например в циклах, где делается множество обращений к перемен
+ной в TLS, можно загрузить глобальную переменную в стековую.
+
+У такого подхода есть два важных преимущества. Во-первых, языки,
+по умолчанию использующие разделение, должны тщательно синхро
+низировать доступ к глобальным данным; для `perThread` же это необяза
+тельно, потому что у каждого потока есть ее локальная копия. Во-вто
+рых, квалификатор `shared` означает, что и система типов, и програм
+мист в курсе, что к переменной `perProcess` одновременно обращаются
+многие потоки. В частности, система типов активно защищает разде
+ляемые данные, запрещая использовать их очевидно некорректным об
+разом. D переворачивает традиционные представления с ног на голову:
+в режиме разделения по умолчанию программист обязан вручную от
+слеживать, какие данные разделяются, а какие нет, и ведь на самом де
+ле, большинство ошибок, имеющих место при параллельных вычисле
+ниях, бывают вызваны чрезмерным или незащищенным разделением
+данных. В режиме явного разделения программист точно знает, что
+данные, не помеченные квалификатором `shared`, действительно будут
+видны только одному потоку. (Для обеспечения такой гарантии значе
+ния с пометкой `shared` проходят дополнительные проверки, до которых
+мы скоро доберемся.)
+
+Использование разделяемых данных остается делом не для новичков,
+поскольку, хотя система типов и обеспечивает низкоуровневую коге
+рентность, автоматически обеспечить соблюдение высокоуровневых
+инвариантов невозможно. Наиболее предпочтительный метод органи
+зации безопасного, простого и эффективного обмена информацией меж
+ду потоками – использовать парадигму *обмена сообщениями*. Обладаю
+щие изолированной памятью потоки взаимодействуют, отправляя друг
+другу асинхронные сообщения, состоящие попросту из совместно упа
+кованных значений D.
+
+Изолированные работники, общающиеся друг с другом с помощью про
+стых каналов коммуникации, – это очень надежный, проверенный вре
+менем подход к параллелизму. Язык Erlang и приложения, использую
+щие спецификацию интерфейса передачи сообщений (Message Passing
+Interface, MPI), применяют его уже давно.
+
+> Намажем мед на пластырь[^6]. Даже в языках, использующих разделение дан
+ных по умолчанию, хорошая практика программирования фактически
+предписывает изолировать потоки. Герб Саттер, известный эксперт по па
+раллельным вычислениям, в статье с красноречивым названием «Исполь
+зуйте потоки правильно = изоляция + асинхронные сообщения» пишет:
+«Потоки – это низкоуровневый инструмент для выражения асинхронных
+действий. „Приподнимите“ их, установив строгую дисциплину: старайтесь
+делать их данные локальными, а синхронизацию и обмен информацией ор
+ганизовывать через асинхронные сообщения. Всякий поток, которому нуж
+но получать информацию от других потоков или от людей, должен иметь
+очередь сообщений (простую очередь FIFO или очередь с приоритетами) и ор
+ганизовывать свою работу, ориентируясь на управляемую событиями пом
+повую магистраль сообщений; замена запутанной логики событийной логи
+кой – чудесный способ улучшить ясность и детерминированность кода.»
+
+Если и есть что-то, чему нас научили десятилетия компьютерных вы
+числений, так это то, что программирование на базе дисциплины не
+масштабируется[^7]. Но пользователи D могут вздохнуть с облегчением:
+в данной цитате в основном очень точно изложены тезисы нескольких
+следующих частей – кроме того, что касается дисциплины.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.4. Запускаем поток
+
+Для запуска потока воспользуйтесь функцией `spawn`, как здесь:
+
+```d
+import std.concurrency, std.stdio;
+
+void main()
+{
+    auto low = 0, high = 100;
+    spawn(&fun, low, high);
+    foreach (i; low .. high)
+    {
+        writeln("Основной поток: ", i);
+    }
+}
+
+void fun(int low, int high)
+{
+    foreach (i; low .. high)
+    {
+        writeln("Дочерний поток: ", i);
+    }
+}
+```
+
+Функция `spawn` принимает адрес функции `fun` и список аргументов `‹a1›`,
+`‹a2›`, ..., `‹an›`. Число аргументов `n` и их типы должны соответствовать сиг
+натуре функции `fun`, иными словами, вызов `fun(‹a1›, ‹a2›, ..., ‹an›)` дол
+жен быть корректным. Эта проверка выполняется во время компиля
+ции. `spawn` создает новый поток выполнения, который инициирует вы
+зов `fun(‹a1›, ‹a2›, ..., ‹an›)`, а затем завершает свое выполнение. Конечно
+же, функция `spawn` не ждет, когда поток закончит выполняться, – она
+возвращает управление сразу же после создания потока и передачи ему
+аргументов (в данном случае двух целых чисел).
+
+Эта программа выводит в стандартный поток вывода в общей сложно
+сти 200 строк. Порядок следования этих строк зависит от множества
+факторов; вполне возможно, что вы увидите 100 строк из основного по
+тока, а затем 100 строк из побочного, в точности противоположную по
+следовательность или некоторое чередование, кажущееся случайным.
+Однако в одной строке никогда не появится смесь из двух сообщений.
+Потому что функция `writeln` определена так, чтобы каждый вызов был
+атомарен по отношению к потоку вывода. Кроме того, порядок строк,
+в котором они порождаются каждым из потоков, также будет соблюден.
+
+Даже если выполнение `main` завершится до окончания выполнения `fun`
+в дочернем потоке, программа будет терпеливо ждать того момента, ко
+гда завершатся все потоки, и только тогда завершит свое выполнение.
+Ведь библиотека поддержки времени исполнения подчиняется неболь
+шому протоколу завершения выполнения программ, о котором мы по
+говорим позже; а пока лишь возьмем на заметку, что даже если `main` воз
+вращает управление, другие потоки не умирают тут же.
+
+Как и было обещано, у только что созданного потока нет ничего общего
+с потоком, инициировавшим его. Ну, почти ничего: глобальный де
+скриптор файла `stdout` *де факто* разделяется между всеми потоками.
+Но все же жульничества тут нет: если вы взглянете на реализацию мо
+дуля `std.stdio`, то увидите, что `stdout` определяется там как глобальная
+разделяемая переменная. Все грамотно просчитано в системе типов.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+### 13.4.1. Неизменяемое разделение
+
+Какие именно функции можно вызывать из `spawn`? Установка на отсут
+ствие разделения налагает определенные ограничения: в функцию, за
+пускающую поток (в рассмотренном выше примере это функция `fun`),
+параметры можно передавать лишь по значению. Любая передача по
+ссылке, как явная (в виде параметра с квалификатором `ref`), так и неяв
+ная (например, с помощью массива), должна быть под запретом. Имея
+в виду это условие, обратимся к новой версии предыдущего примера:
+
+```d
+import std.concurrency, std.stdio;
+
+void main()
+{
+    auto low = 0, high = 100;
+    auto message = "Да, привет #";
+    spawn(&fun, message, low, high);
+    foreach (i; low .. high)
+    {
+        writeln("Основной поток: ", message, i);
+    }
+}
+
+void fun(string text, int low, int high)
+{
+    foreach (i; low .. high)
+    {
+        writeln("Дочерний поток: ", text, i);
+    }
+}
+```
+
+Переписанный пример идентичен исходному, за исключением того, что
+печатает еще одну строку. Эта строка создается в основном потоке и пе
+редается в дочерний поток без копирования. По сути, содержание `message`
+разделяется между потоками. Таким образом, нарушен вышеупомяну
+тый принцип, гласящий, что любое разделение данных должно быть
+явно помечено ключевым словом `shared`. Тем не менее код этого примера
+компилируется и запускается. Что же происходит?
+
+В главе 8 сообщается, что квалификатор `immutable` предоставляет серь
+езные гарантии: гарантируется, что помеченное этим ключевым словом
+значение ни разу не изменится за всю свою жизнь. В той же главе объ
+ясняется (см. раздел 8.2), что тип `string` – это на самом деле псевдоним
+для типа `immutable(char)[]`. Наконец, мы знаем, что все споры возникают
+из-за разделения *изменяемых* данных – пока никто данные не изменя
+ет, можно свободно разделять их, ведь все будут видеть в точности одно
+и то же. Система типов и инфраструктура потоков в целом признают
+этот факт, разрешая разделять между потоками все данные, помечен
+ные квалификатором `immutable`. В частности, можно разделять значе
+ния типа `string`, отдельные знаки которых изменить невозможно. На
+самом деле, своим появлением в языке квалификатор `immutable` не в по
+следнюю очередь обязан той помощи, которую он оказывает при разде
+лении структурированных данных между потоками.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.5. Обмен сообщениями между потоками
+
+Потоки, печатающие сообщения в произвольном порядке, малоинтерес
+ны. Изменим наш пример так, чтобы обеспечить работу потоков в тан
+деме. Добьемся, чтобы они печатали сообщения следующим образом:
+
+```
+Основной поток: 0
+Дочерний поток: 0
+Основной поток: 1
+Дочерний поток: 1
+...
+Основной поток: 99
+Дочерний поток: 99
+```
+
+Для этого потребуется определить небольшой протокол взаимодейст
+вия двух потоков: основной поток должен отправлять дочернему пото
+ку сообщение «Напечатай это число», а дочерний – отвечать «Печать
+завершена». Вряд ли здесь имеют место какие-либо параллельные вы
+числения, но такой пример наглядно объясняет, как организуется взаи
+модействие в чистом виде. В настоящих приложениях большую часть
+своего времени потоки должны заниматься полезной работой, а на об
+щение тратить лишь сравнительно малое время.
+
+Начнем с того, что для взаимодействия двух потоков им требуется знать,
+как обращаться друг к другу. В программе может быть много перегова
+ривающихся потоков, так что средство идентификации необходимо.
+Чтобы обратиться к потоку, нужно получить возвращаемый функцией
+`spawn` *идентификатор потока* (*thread id*), который с этих пор мы будем
+неофициально называть «tid». (Тип tid так и называется – `Tid`.) Дочер
+нему потоку, в свою очередь, также нужен tid, для того чтобы отпра
+вить ответ. Это легко организовать, заставив отправителя указать соб
+ственный `Tid`, как пишут адрес отправителя на конверте. Вот этот код:
+
+```d
+import std.concurrency, std.stdio, std.exception;
+
+void main()
+{
+    auto low = 0, high = 100;
+    auto tid = spawn(&writer);
+    foreach (i; low .. high)
+    {
+        writeln("Основной поток: ", i);
+        tid.send(thisTid, i);
+        enforce(receiveOnly!Tid() == tid);
+    }
+}
+
+void writer()
+{
+    for (;;)
+    {
+        auto msg = receiveOnly!(Tid, int)();
+        writeln("Дочерний поток: ", msg[1]);
+        msg[0].send(thisTid);
+    }
+}
+```
+
+Теперь функции `writer` аргументы не нужны: всю необходимую инфор
+мацию она получает в форме сообщений. Основной поток сохраняет `Tid`,
+возвращенный функцией `spawn`, а затем использует его при вызове мето
+да `send`. С помощью этого вызова другому потоку отправляются два
+фрагмента данных: `Tid` текущего потока (доступ к которому предостав
+ляет глобальная переменная `thisTid`) и целое число, которое нужно на
+печатать. Перекинув данные через забор другому потоку, основной по
+ток начинает ждать подтверждение того, что его сообщение получено,
+в виде вызова `receiveOnly`. Функции `send` и `receiveOnly` работают в танде
+ме: всякому вызову `send` в одном потоке ставится в соответствие вызов
+`receiveOnly` в другом. В названии `receiveOnly` присутствует слово «only»
+(только), потому что `receiveOnly` принимает только определенные типы,
+например, инициатор вызова `receiveOnly!bool()` принимает лишь сооб
+щения в виде логических значений; если другой поток отправляет что-
+либо другое, `receiveOnly` порождает исключение типа `MessageMismatch`.
+
+Предоставим `main` копаться в цикле `foreach` и сосредоточимся на функ
+ции `writer`, реализующей вторую часть нашего мини-протокола. `writer`
+коротает время в цикле, начинающемся получением сообщения, кото
+рое должно состоять из значения типа `Tid` и значения типа `int`. Именно
+это обеспечивает вызов `receiveOnly!(Tid, int)()`; опять же, если бы основ
+ной поток отправил сообщение с каким-либо иным количеством аргу
+ментов или с аргументами других типов, `receiveOnly` прервала бы свое
+выполнение по исключению. Как уже говорилось, вызов `receiveOnly`
+в теле `writer` полностью соответствует вызову `tid.send(thisTid, i)` из `main`.
+
+Типом `msg` является `Tuple!(Tid, int)`. В общем случае сообщения со мно
+жеством аргументов упаковываются в кортежи, так что одному члену
+кортежа соответствует один аргумент. Но если сообщение состоит всего
+из одного значения, лишние движения не нужны, и упаковка в `Tuple`
+опускается. Например, `receiveOnly!int()` возвращает `int`, а не `Tuple!int`.
+
+Продолжим разбор `writer`. Следующая строка, собственно, выполняет
+печать (запись в консоль). Вспомните, что для кортежа `msg` выражение
+`msg[0]` означает обращение к первому члену кортежа (то есть к `Tid`), а вы
+ражение `msg[1]` – доступ к его второму члену (к целому числу). Наконец,
+`writer` посылает уведомление о том, что завершила запись в консоль, по
+просту отправляя собственный `Tid` отправителю предыдущего сообще
+ния – своего рода пустой конверт, лишь подтверждающий личность от
+правителя. «Да, я получил твое сообщение, – подразумевает пустое
+письмо, – и принял соответствующие меры. Твоя очередь.» Основной
+поток не продолжит работу, пока не получит такое уведомление, но как
+только это произойдет, цикл начнет выполняться дальше.
+
+Отправлять `Tid` дочернего потока назад в данном случае излишне – хва
+тило бы любой болванки, например `int` или `bool`. Однако в общем случае
+в программе есть много потоков, отправляющих друг другу сообщения,
+так что самоидентификация становится важна.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.6. Сопоставление по шаблону с помощью receive
+
+Большинство полезных протоколов взаимодействия сложнее, чем опре
+деленный выше. Возможности, которые предоставляет `receiveOnly`, весь
+ма ограничены. Например, с помощью `receiveOnly` довольно сложно реа
+лизовать такой маневр, как «получить `int` или `string`».
+
+Гораздо более мощным примитивом является функция `receive`, которая
+сопоставляет и диспетчирует сообщения в зависимости от их типа. Ти
+пичный вызов `receive` выглядит так:
+
+```d
+receive(
+    (string s) { writeln("Получена строка со значением ", s); },
+    (int x) { writeln("Получено число со значением ", x); }
+);
+```
+
+При сопоставлении этого вызова со следующими вызовами `send` во всех
+случаях будет наблюдаться совпадение:
+
+```d
+send(tid, "здравствуй");
+send(tid, 5);
+send(tid, 'a');
+send(tid, 42u);
+```
+
+Первый вызов `send` соответствует типу `string` и направляется в литерал
+функции, определенный в `receive` первым; остальные три вызова соот
+ветствуют типу `int` и передаются во второй функциональный литерал.
+Кстати, в качестве функций-обработчиков необязательно использовать
+литералы – какие-то (или даже все) обработчики могут быть адресами
+именованных функций:
+
+```d
+void handleString(string s) { ... }
+receive(
+    &handleString,
+    (int x) { writeln("Получено число со значением ", x); }
+);
+```
+
+Сопоставление не является досконально точным; вместо того чтобы тре
+бовать точного совпадения, соблюдают обычные правила перегрузки,
+в соответствии с которыми `char` и `uint` могут быть неявно преобразованы
+в `int`. При сопоставлении следующих вызовов соответствие, напротив,
+обнаружено *не будет*:
+
+```d
+send(tid, "hello"w); // Строка в кодировке UTF-16 (см. раздел 4.5)
+send(tid, 5L);       // long
+send(tid, 42.0);     // double
+```
+
+Когда функция `receive` видит сообщение неожиданного типа, она не по
+рождает исключение (как это делает `receiveOnly`). Подсистема обмена со
+общениями просто сохраняет неподходящие сообщения в очереди, в на
+роде называемой *почтовым ящиком* (*mailbox*) потока. `receive` терпели
+во ждет, когда в почтовом ящике появится сообщение нужного типа.
+Такая политика делает `receive` и протоколы, реализованные на базе
+этой функции, более гибкими, но и более восприимчивыми к блокиро
+ванию и переполнению ящика. Одно недоразумение при обмене инфор
+мацией – и в ящике потока начнут накапливаться сообщения не тех
+типов, а `receive` тем временем будет ждать сообщения, которое никогда
+не придет.
+
+Пользуясь посредническими услугами `Tuple`, дуэт `send`/`receive` с легко
+стью обрабатывает и группы аргументов. Например:
+
+```d
+receive(
+    (long x, double y) { ... },
+    (int x) { ... }
+);
+```
+
+соответствуют те же сообщения, что и
+
+```d
+receive(
+    (Tuple!(long, double) tp) { ... },
+    (int x) { ... }
+);
+```
+
+Такой вызов, как `send(tid, 5, 6.3)`, соответствует первому функциональ
+ному литералу как первого, так и второго предыдущих примеров.
+
+Существует особая версия `receive` – функция `receiveTimeout`, позволяю
+щая потоку предпринять экстренные меры в случае задержки сообще
+ний. У `receiveTimeout` есть «срок годности»: она завершает свое выполне
+ние по истечении указанного промежутка времени. Об истечении «от
+пущенного времени» `receiveTimeout` сообщает, возвращая `false`:
+
+```d
+auto gotMessage = receiveTimeout(
+    1000, // Время в милисекундах
+    (string s) { writeln("Получена строка со значением ", s); },
+    (int x) { writeln("Получено число со значением ", x); }
+);
+
+if (!gotMessage) {
+    stderr.writeln("Выполнение прервано по прошествии одной секунды.");
+}
+```
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+### 13.6.1. Первое совпадение
+
+Рассмотрим пример:
+
+```d
+receive(
+    (long x) { ... },
+    (string x) { ... },
+    (int x) { ... }
+);
+```
+
+Такой вызов не скомпилируется: `receive` отвергает этот вызов, посколь
+ку третий обработчик недостижим при любых условиях. Любое отправ
+ленное по каналу передачи значение типа `int` застревает в первом обра
+ботчике.
+
+Порядок аргументов `receive` определяет, каким образом осуществляют
+ся попытки сопоставления. Принцип тот же, что и при вычислении бло
+ков `catch` в инструкции `try`, но не при объектно-ориентированной диспет
+черизации функций. Единого мнения насчет относительных преиму
+ществ и недостатков использования первого совпадения или же лучше
+го совпадения – нет; достаточно сказать, что, по всей видимости, первое
+совпадение хорошо подходит для этого конкретного случая `receive`.
+
+Выполнение принципа первого совпадения обеспечивается функцией
+`receive` с помощью простого анализа, выполняемого во время компиля
+ции. Для любых типов сообщения `‹Сбщ1›` и `‹Сбщ2›` справедливо, что, если
+в вызове `receive` обработчик `‹Сбщ2›` следует после обработчика `‹Сбщ1›`,
+`receive` гарантирует, что тип `‹Сбщ2›` *невозможно* неявно преобразовать
+в тип `‹Сбщ1›`. Если можно, то это означает, что обработчик `‹Сбщ1›` будет ло
+вить сообщения `‹Сбщ2›`, так что в компиляции такому вызову будет отка
+зано. Выполнение этой проверки для предыдущего примера завершает
+ся неудачей в процессе той итерации, когда `‹Сбщ1›` присваивается значе
+ние `long`, а `‹Сбщ2›` – `int`.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+### 13.6.2. Соответствие любому сообщению
+
+Что если бы вы пожелали обеспечить просмотр абсолютно всех сообще
+ний в почтовом ящике – например, для уверенности в том, что он не пе
+реполнится мусором?
+
+Ответ прост – нужно всего лишь включить обработчик сообщений типа
+`Variant` последним в список аргументов `receive`. Например:
+
+```d
+receive(
+    (long x) { ... },
+    (string x) { ... },
+    (double x, double y) { ... },
+    ...
+    (Variant any) { ... }
+);
+```
+
+Тип `Variant`, определенный в модуле `std.variant`, – это динамический
+тип, вмещающий ровно одно значение любого другого типа. `receive` вос
+принимает `Variant` как обобщенный контейнер для любого типа сообще
+ния, а потому вызов `receive` с обработчиком для типа `Variant` всегда бу
+дет отработан, если в очереди есть хотя бы одно сообщение.
+
+Расположить обработчик `Variant` в конце цепочки обработки сообще
+ний – хороший способ избавить ваш почтовый ящик от случайных со
+общений.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.7. Копирование файлов – с выкрутасом
+
+Напишем коротенькую программу для копирования файлов – один из
+популярных способов познакомиться с интерфейсом языка файловой
+системы. Классический пример в стиле Кернигана и Ричи целиком на
+паре команд `getchar`/`putchar`! [34, глава 1, с. 15]. Конечно же, чтобы уско
+рить передачу, «родные» программы системы, копирующие файлы,
+практикуют буферное чтение и буферную запись, а также используют
+множество других методов оптимизации, так что написать конкурен
+тоспособную программу было бы сложно, однако параллельные вычис
+ления нам помогут.
+
+Обычный способ копирования файлов:
+
+1. Прочесть данные из исходного файла и поместить в буфер.
+2. Если ничего не было прочитано, копирование завершено.
+3. Записать данные из буфера в целевой файл.
+4. Повторить заново, начиная с шага 1.
+
+Добавление соответствующей обработки ошибок завершит полезную
+(но не оригинальную) программу. Если размер буфера будет выбран дос
+таточно большим, а оба файла (и источник, и целевой файл) окажутся
+на одном и том же диске, быстродействие этого алгоритма приблизится
+к оптимальному.
+
+В наше время файловыми хранилищами могут быть многие физические
+устройства: жесткие диски, флеш-диски, оптические диски, подсоеди
+ненные смартфоны, а также сетевые сервисы удаленного доступа. Эти
+устройства характеризуются разнообразными показателями задержки
+и скорости и подключаются с помощью разных аппаратных и программ
+ных интерфейсов. Такие интерфейсы могут работать параллельно (а не
+по одному в каждый момент времени, как предписывает алгоритм в сти
+ле «прочесть данные из буфера/записать данные в буфер»), и именно
+так и нужно их использовать. В идеале должна поддерживаться макси
+мальная занятость как устройства-источника, так и устройства-полу
+чателя, что мы можем изобразить как два потока, работающих по про
+токолу «поставщик/потребитель»:
+
+1. Породить один дочерний поток, который в цикле ждет сообщений,
+содержащих буферы памяти, и записывает их в целевой файл.
+2. Прочесть данные из исходного файла и разместить их в заново соз
+данном буфере.
+3. Если ничего не было прочитано, копирование завершено.
+4. Отправить дочернему потоку сообщение, содержащее буфер с прочи
+танными данными.
+5. Повторить, начав с шага 2.
+
+С таким подходом один поток будет работать с источником, а другой –
+с приемником. В зависимости от природы «исходного пункта» и «пунк
+та назначения» можно получить значительное ускорение. Если скоро
+сти устройств сравнимы и невелики относительно пропускной способ
+ности шины памяти, теоретически скорость копирования может быть
+удвоена. Напишем простую программу, которая реализует модель «по
+ставщик/потребитель» и копирует содержимое стандартного потока
+ввода в стандартный поток вывода:
+
+```d
+import std.concurrency, std.stdio;
+
+void main()
+{
+    enum bufferSize = 1024 * 100;
+    auto tid = spawn(&fileWriter);
+    // Цикл чтения
+    foreach (ubyte[] buffer; stdin.byChunk(bufferSize))
+    {
+        send(tid, buffer.idup);
+    }
+}
+
+void fileWriter()
+{
+    // Цикл записи
+    for (;;)
+    {
+        auto buffer = receiveOnly!(immutable(ubyte)[])();
+        stdout.rawWrite(buffer);
+    }
+}
+```
+
+В этой программе данные из основного потока передаются в дочерний
+поток посредством разделения неизменяемых данных: передаваемые
+сообщения имеют тип `immutable(ubyte)[]`, то есть являются массивами
+неизменяемых значений типа `ubyte`. Эти буферы создаются в цикле
+`foreach` при чтении данных из входного потока порциями, каждая из ко
+торых имеет тип `immutable(ubyte)[]` и размер `bufferSize`. На каждом про
+ходе цикла функция `byChunk` читает данные во временный буфер (пере
+менную `buffer`), неизменная копия которого создается свойством `idup`.
+Большую часть тяжелой работы выполняет управляющая часть `foreach`;
+на долю тела этой конструкции остается лишь создание копии и от
+правка буфера дочернему потоку. Как уже говорилось, передача данных
+между потоками возможна благодаря присутствию квалификатора
+`immutable`; если заменить `idup` на `dup`, вызов `send` не скомпилируется.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.8. Останов потока
+
+В приводившихся до сих пор примерах есть кое-что необычное, в част
+ности в функции `writer`, определенной в разделе 13.5, и в только что опре
+деленной функции `fileWriter` из раздела 13.7: обе функции содержат
+бесконечный цикл. На самом деле, повнимательнее взглянув на пример
+с копированием файлов, можно заметить, что `main` и `fileWriter` прекрас
+но понимают друг друга в разговоре о копировании, но никогда не обсу
+ждают друг с другом останов приложения; другими словами, `main` нико
+гда не говорит `fileWriter`: «Дело сделано, собирайся и пойдем домой».
+
+Останов многопоточных приложений всегда был делом мудреным. По
+ток легко запустить, но запустив, трудно остановить; завершение рабо
+ты приложения – событие асинхронное и может застать приложение за
+выполнением совершенно произвольной операции. Низкоуровневые API
+для работы с потоками предоставляют средство для принудительного
+останова потоков, неизменно сопровождая его предупреждением о чрез
+мерной грубости этого инструмента и рекомендацией об использовании
+какого-нибудь более высокоуровневого протокола завершения работы.
+
+D предоставляет простой и надежный протокол останова потоков. Каж
+дый поток обладает *потоком-владельцем*; по умолчанию владельцем
+считается поток, инициировавший вызов функции `spawn`. Владельца те
+кущего потока можно изменить динамически, сделав вызов вида `setOwner(tid)`. У каждого потока только один владелец, но сам он может быть
+владельцем множества потоков.
+
+Самое важное проявление отношения «владелец/собственность» за
+ключается в том, что по завершении выполнения потока-владельца вы
+зовы функции `receive` в дочернем потоке начнут порождать исключения
+типа `OwnerTerminated`. Исключение порождается, только если в очереди
+к `receive` больше нет подходящих сообщений и необходимо ждать при
+хода новых; пока у `receive` есть что извлечь из ящика, она не породит
+исключение `OwnerTerminated`. Другими словами, при останове потока-
+владельца вызовы `receive` (или `receiveOnly`, коли на то пошло) в дочерних
+потоках породят исключения тогда и только тогда, когда в противном
+случае они заблокируют выполнение программы, так как продолжат
+ожидать сообщение, которое никогда не придет. Отношение владения
+необязательно однонаправленно. В действительности, возможна ситуа
+ция, когда два потока являются владельцами друг друга; в таком слу
+чае, какой бы поток ни завершался первым, он оповестит другой поток.
+
+Окинем программу копирования файлов свежим взглядом – с учетом
+знания об отношении владения. В любой заданный момент времени в по
+лете между основным и второстепенным потоками находится масса сооб
+щений. Чем быстрее выполняются операции чтения по сравнению с опе
+рациями записи, тем больше буферов будет находиться в почтовом ящи
+ке записывающего потока в ожидании обработки. Возврат из `main` заста
+вит `receive` породить исключение, но не раньше, чем будут обработаны
+ожидающие сообщения. Сразу же после того, как ящик записывающего
+потока опустеет (а последняя порция данных будет записана в целевой
+файл), очередной вызов `receive` породит исключение. Записывающий по
+ток прекращает выполнение по исключению `OwnerTerminated`; система
+времени исполнения в курсе, что это за исключение, и просто его игнори
+рует. Операционная система закрывает стандартные потоки ввода и вы
+вода так, как обычно, и операция копирования успешно завершается.
+
+Может показаться, что в промежутке между моментом отправки по
+следнего сообщения из `main` и моментом возврата из `main` (что заставляет
+`receive` породить исключение) возникает гонка. Что если исключение
+«опередит» последнее сообщение – или, хуже того, несколько послед
+них сообщений? На самом деле никакой гонки нет. Поток, отправляю
+щий сообщения, всегда думает о последствиях: последнее сообщение
+помещается в конец очереди дочернего потока *до* того, как исключение
+`OwnerTerminated` начнет свой путь (фактически распространение исклю
+чения организуется при помощи той же очереди, что и в случае обыч
+ных сообщений). Однако гонка *присутствовала бы*, если бы функция
+`main` завершала свое выполнение в тот самый момент, когда другой, тре
+тий поток отправлял бы сообщения в очередь `fileWriter`.
+
+Подобная же цепочка рассуждений показывает, что наш предыдущий
+простой пример, в котором два потока «в ногу» записывают 200 сообще
+ний, также корректен: функция `main` завершает свое выполнение после
+отправки (ждет до конца) последнего сообщения дочернему потоку. До
+черний поток сначала опустошает очередь, а затем заканчивает работу
+по исключению `OwnerTerminated`.
+
+Если вы считаете, что для механизма, обрабатывающего завершение
+выполнения потока, порождение исключения – выбор слишком суро
+вый, то помните, что никто не лишал вас возможности обработать `OwnerTerminated` явно:
+
+```d
+// Завершается без исключения
+void fileWriter()
+{
+    // Цикл записи
+    for (bool running = true; running; )
+    {
+        receive(
+            (immutable(ubyte)[] buffer) { tgt.write(buffer); },
+            (OwnerTerminated) { running = false; }
+        );
+    }
+    stderr.writeln("Выполнение завершено без приключений.");
+}
+```
+
+В данном случае по завершении выполнения `main` поток `fileWriter` мирно
+возвращает управление, и все счастливы. Но что произойдет, если ис
+ключение породит дочерний, записывающий поток? Если возникнут
+проблемы с записью данных в `tgt`, вызов функции `write` может завер
+шиться неудачей. В таком случае вызов `send` из основного потока также
+завершится неудачей (а именно будет порождено исключение типа `OwnerFailed`), то есть произойдет как раз то, что ожидалось. Кстати, если
+дочерний поток завершит свое выполнение обычным способом (а не по
+исключению), последующие вызовы `send`, отправлявшие сообщения это
+му потоку, также завершатся неудачей, но с другим типом исключе
+ния – `OwnedTerminated`.
+
+Рассмотренная программа для копирования файлов более отказоустой
+чива, чем можно предположить, судя по ее простоте. Тем не менее нуж
+но сказать, что протокол завершения выполнения гладко срабатывает
+лишь тогда, когда отношения между потоками просты и предельно по
+нятны, и полагаться на него стоит исключительно в таких случаях.
+А когда в деле замешаны несколько потоков и отношения владения ме
+жду ними отражаются сложным графом, лучше всего организовать
+взаимодействие всех этих потоков по протоколам, предусматривающим
+явное уведомление об окончании обмена данными. В случае примера
+с копированием файлов можно реализовать следующую простую идею:
+установить соглашение, по которому отправка буфера нулевого размера
+записывающему потоку будет означать удачное завершение работы чи
+тающим потоком. Получив такое сообщение и завершив запись, запи
+сывающий поток также уведомляет поток, осуществлявший чтение,
+о своем завершении. После чего «читатель», наконец, тоже может за
+вершить свое выполнение. Такой протокол явного уведомления хорошо
+масштабируется до случаев, когда по пути от «читателя» к «писателю»
+данные обрабатываются множеством других потоков.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.9. Передача нештатных сообщений
+
+Допустим, с помощью предположительно прыткой программы, кото
+рую мы только что написали, вы копируете большой файл из быстрого
+локального хранилища на медленный сетевой диск. На полпути возни
+кает ошибка чтения – файл поврежден. Это заставляет `read`, а затем
+и `main` породить исключения, и все происходит тогда, когда множество
+буферов находятся в полете, но еще не записаны. Более абстрактно, мы
+видели, что если поток-владелец завершит свое выполнение *обычным способом*, любой блокирующий вызов `receive` из принадлежащих ему
+потоков породит исключение. Но что произойдет, если владелец завер
+шит выполнение по исключению?
+
+Если поток завершается посредством порождения исключения, это знак
+серьезной проблемы, о которой с должной настойчивостью нужно уве
+домить дочерние потоки. И, конечно, это выполняется с помощью *нештатного* сообщения.
+
+Вспомните, что функция `receive` заботится лишь о сообщениях, совпав
+ших с заданными шаблонами, а остальным позволяет накапливаться
+в очереди. Есть способ внести в это поведение поправку. Поток-отправи
+тель может инициировать обработку сообщения потоком-получателем,
+вызвав функцию `prioritySend` вместо `send`. Эти две функции принимают
+одни и те же параметры, но ведут себя по-разному, что в действительно
+сти отражается на поведении получателя. Передача сообщения типа `T`
+с помощью `prioritySend` заставляет `receive` в потоке-получателе действо
+вать следующим образом:
+
+- Если вызов `receive` предусматривает обработку типа `T`, то сообщение
+с приоритетом будет извлечено сразу же после завершения обработки
+текущего сообщения – даже если сообщение с приоритетом пришло
+позже других обычных (неприоритетных) сообщений. Сообщения
+с приоритетом всегда помещаются в начало очереди, так что послед
+нее пришедшее сообщение с приоритетом всегда извлекается функ
+цией `receive` первым (даже если другие сообщения с приоритетом
+уже ждут).
+- Если вызов `receive` не обрабатывает тип `T` (то есть совокупность ука
+занных обстоятельств предписывает `receive` оставить сообщение та
+кого типа в почтовом ящике в ожидании) и `T` является наследником
+`Exception`, то `receive` напрямую порождает извлеченное сообщение-
+исключение.
+- Если вызов `receive` не обрабатывает тип `T` и `T` не является наследни
+ком `Exception`, то `receive` порождает исключение типа `PriorityMessageException!T`. Объект этого исключения содержит копию полученно
+го сообщения в виде внутреннего элемента `message`.
+
+Если поток завершается по исключению, исключение `OwnerFailed` рас
+пространяется на все потоки, которыми он владеет, с помощью вызова
+`prioritySend`. В программе копирования файлов порождение исключе
+ния внутри `main` вызывает порождение исключения и внутри `fileWriter`
+(как только там будет вызвана функция `receive`); в результате, напеча
+тав сообщение об ошибке и вернув ненулевой код выхода, останавлива
+ется весь процесс. В отличие от случая с «нормальным» завершением
+исполнения, в данной ситуации вполне допустимо, что в подвешенном
+состоянии останутся буферы, которые были уже прочитаны, но еще не
+записаны.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.10. Переполнение почтового ящика
+
+Программа для копирования файлов на основе протокола «поставщик/
+потребитель» работает достаточно хорошо, однако обладает одним важ
+ным недостатком. Рассмотрим копирование большого файла, при кото
+ром данные передаются между устройствами, скорость доступа к кото
+рым существенно различается, например копирование приобретенного
+законным способом файла с фильмом с внутреннего диска (быстрый дос
+туп) на сетевой диск (вероятно, значительно более медленный доступ).
+В этом случае поставщик (основной поток, выполняющий функцию
+`main`) порождает буферы со значительной скоростью, гораздо более вы
+сокой, чем скорость, с которой потребитель в состоянии записать их
+в целевой файл. Разница в скоростях вызывает скопление данных, на
+прасно занимающих память, которую программа не может использо
+вать для повышения производительности.
+
+Во избежание переполнения почтового ящика, API для параллельных
+вычислений позволяет задать максимальный размер очереди сообще
+ний, а также действие, предпринимаемое при достижении этого преде
+ла. Соответствующие сигнатуры выглядят так:
+
+```d
+// Внутри std.concurrency
+void setMaxMailboxSize(Tid tid, size_t messages, bool function(Tid) onCrowdingDoThis);
+```
+
+Вызывая `setMailboxSize`, вы устанавливаете для подсистемы параллель
+ных вычислений правило: всякий раз когда требуется отправить новое
+сообщение, а очередь уже содержит число сообщений, указанное в `messages`, вызывать `onCrowdingDoThis(tid)`. Если `onCrowdingDoThis(tid)` возвра
+щает `false` или порождает исключение, новое сообщение игнорируется.
+В противном случае еще раз проверяется размер очереди потока, и если
+выясняется, что он уже меньше, чем размер `messages`, новое сообщение
+доставляется потоку с идентификатором `tid`. В противном случае весь
+цикл возобновляется.
+
+Вызов `setMaxMailboxSize` выполняется в потоке, осуществляющем вы
+зов, а не в потоке, этот вызов принимающем. Иными словами, поток,
+инициирующий отправку сообщения, также является ответственным
+и за принятие экстренных мер при переполнении почтового ящика по
+лучателя. Кажется логичным спросить: почему нельзя расположить
+этот вызов в потоке-получателе? При расширении масштаба, а именно
+применительно к программам с большим количеством потоков, такой
+подход породил бы порочные последствия: потоки, пытающиеся отпра
+вить сообщения, угрожали бы лишить трудоспособности потоки с пол
+ными ящиками.
+
+Есть ряд предопределенных действий, предпринимаемых в случае, ес
+ли почтовый ящик полон: заблокировать отправителя до тех пор, пока
+очередь не станет меньше, породить исключение или проигнорировать
+новое сообщение. Такие предопределенные действия удобно упакованы:
+
+```d
+// Внутри std.concurrency
+enum OnCrowding { block, throwException, ignore }
+void setMaxMailboxSize(Tid tid, size_t messages, OnCrowding doThis);
+```
+
+В нашем случае лучше всего попросту блокировать поток-читатель, как
+только ящик становится слишком большим. Добиться этого можно,
+вставив вызов
+
+```d
+setMaxMailboxSize(tid, 1024, OnCrowding.block);
+```
+
+сразу же после вызова `spawn`.
+
+В следующих разделах описываются подходы к организации межпоточ
+ной передачи данных, служащие или альтернативой, или дополнением
+к обмену сообщениями. Обмен сообщениями – рекомендуемый метод
+организации межпоточного взаимодействия; этот метод легок для пони
+мания, порождает удобный для чтения код, является надежным и мас
+штабируемым. К более низкоуровневым механизмам стоит обращаться
+лишь в совершенно особых обстоятельствах – и не забывайте, что «осо
+бые» обстоятельства не всегда настолько особые, какими кажутся.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.11. Квалификатор типа shared
+
+Мы уже познакомились с квалификатором `shared` в разделе 13.3. Для
+системы типов ключевое слово `shared` служит сигналом о том, что не
+сколько потоков обладают доступом к одному фрагменту данных. Ком
+пилятор тоже признает этот факт и соответственно реагирует, накла
+дывая ограничения на операции с разделяемыми данными, а также
+посредством генерации особого кода для разрешенных операций.
+
+С помощью глобального определения
+
+```d
+shared uint threadsCount;
+```
+
+в программу на D вводится значение типа `shared(uint)`, что соответству
+ет глобально определенному целому числу без знака в программе на C.
+Такая переменная видима всем потокам в системе. Примечание в виде
+shared здорово помогает компилятору: язык «знает», что `threadsCount` от
+крыт для свободного доступа множеству потоков, и запрещает обраще
+ния к этой переменной наивными способами. Например:
+
+```d
+void bumpThreadsCount()
+{
+    ++threadsCount; // Ошибка! Увеличить на единицу значение типа shared int невозможно!
+}
+```
+
+Что происходит? Где-то внизу, на машинном уровне, `++threadCount` не яв
+ляется атомарной операцией; это сложная операция, представляющая
+собой последовательность трех простых: прочесть – изменить – запи
+сать. Сначала `threadCount` загружается в регистр, затем значение регист
+ра увеличивается на единицу и, наконец, `threadCount` записывается об
+ратно в память. Для обеспечения корректности всей сложной операции
+эти три шага необходимо выполнять единым блоком. Корректный спо
+соб увеличить на единицу разделяемое целое число – воспользоваться
+одним из специализированных атомарных примитивов из модуля `std.concurrency`:
+
+```d
+import std.concurrency;
+shared uint threadsCount;
+
+void bumpThreadsCount()
+{
+    // std.concurrency определяет atomicOp(string op)(ref shared uint, int)
+    atomicOp!"+="(threadsCount, 1); // Все в порядке
+}
+```
+
+Поскольку все разделяемые данные тщательно учитываются и находят
+ся под эгидой языка, передавать данные с квалификатором `shared` разре
+шается с помощью функций `send` и `receive`.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+### 13.11.1. Сюжет усложняется: квалификатор shared транзитивен
+
+В главе 8 объясняется, почему квалификаторы `const` и `immutable` долж
+ны быть *транзитивными* (свойство, также известное как глубина или
+рекурсивность): каким бы косвенным путем вы ни следовали, рассмат
+ривая «внутренности» неизменяемого объекта, сами данные должны
+оставаться неизменяемыми. В противном случае гарантии, предостав
+ляемые квалификатором `immutable`, имели бы силу комментария в коде.
+Нельзя сказать, что нечто «до определенного момента» неизменяемо
+(`immutable`), а дальше меняется. Зато можно говорить, что данные *изменяемы* до определенного момента, а затем становятся совершенно неиз
+меняемыми, вплоть до самых глубоко вложенных элементов. Применив
+квалификатор `immutable`, вы сворачиваете на улицу с односторонним
+движением. Мы уже видели, что присутствие квалификатора `immutable`
+облегчает реализацию многих оправдавших себя идиом, не претендую
+щих на свободу программиста, включая функциональный стиль и раз
+деление данных между потоками. Если бы неизменяемость применя
+лась «до определенного момента», то же самое относилось бы и к кор
+ректности программы.
+
+Точно такой же ход рассуждений применим и для квалификатора `shared`.
+На самом деле, в случае с `shared` необходимость транзитивности абсо
+лютно очевидна. Приведем пример. Выражение
+
+```d
+shared int* pInt;
+```
+
+в соответствии с синтаксисом квалификаторов (см. раздел 8.2) эквива
+лентно выражению
+
+```d
+shared(int*) pInt;
+```
+
+Верная интерпретация `pInt` такова: «Указатель является разделяемым,
+и данные, на которые он указывает, также разделяемы». При поверх
+ностном, нетранзитивном подходе к разделению `pInt` превратился бы
+в «разделяемый указатель на неразделяемую память», и все бы ничего,
+если бы такой тип данных имел хоть какой-то смысл. Это все равно что
+сказать: «Я делюсь этим бумажником со всеми; только, пожалуйста, не
+забывайте, что деньгами из него я делиться не собирался»[^8]. Заявление,
+что потоки разделяют указатель, но не данные, на которые он указыва
+ет, возвращает нас к чудесной парадигме программирования на основе
+системы доверия, которая всегда успешно проваливалась. И причина
+большей части проблем не чьи-то происки, а честные ошибки. Про
+граммное обеспечение имеет большой объем, сложно устроено и посто
+янно изменяется, что плохо сочетается с обеспечением гарантий на ос
+нове соглашений.
+
+Тем не менее есть совершенно логичное понятие «неразделяемый указа
+тель на разделяемые данные». Некоторый поток обладает «личным»
+указателем, а этот указатель «смотрит» на разделяемые данные. Эту
+идею легко выразить синтаксически:
+
+```d
+shared(int)* pInt;
+```
+
+Между нами, если бы существовала премия «За лучшее отображение со
+держания», нотация `квалификатор(тип)` ее бы отхватила. Эта форма записи
+совершенна. Синтаксис просто не позволит создать неправильный ука
+затель. Некорректное сочетание синтаксических единиц выглядит так:
+
+```d
+int shared(*) pInt;
+```
+
+Такое выражение не имеет смысла даже синтаксически, поскольку
+`(*)` – это не тип (ну да, *на самом деле* этот милый смайлик символизиру
+ет циклопа).
+
+Транзитивность квалификатора `shared` действует не только в отноше
+нии указателей, но и в отношении полей объектов-структур и классов:
+поля разделяемого объекта также автоматически воспринимаются как
+помеченные квалификатором `shared`. Подробный разбор порядка взаи
+модействия этого квалификатора с классами и структурами представ
+лен далее в этой главе.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.12. Операции с разделяемыми данными и их применение
+
+Работа с разделяемыми данными необычна, поскольку множество пото
+ков могут читать и записывать разделяемые данные в любой момент. По
+этому компилятор заботится о соблюдении целостности данных и при
+чинности всеми операциями с разделяемыми данными.
+
+Операции чтения и записи разделяемых (`shared`) значений разрешены,
+и гарантированно будут атомарными для следующих типов: числовые
+типы (кроме `real`), указатели, массивы, указатели на функции, делега
+ты и ссылки на классы. Структуру с единственным полем одного из пе
+речисленных типов также можно читать и записывать как неделимый
+объект. Подчеркнутое отсутствие в списке «разрешенных типов» типа
+`real` обусловлено тем, что это единственный тип, зависящий от платфор
+мы. Вот почему в плане атомарного разделения компилятор смотрит на
+`real` с опаской. На машинах Intel `real` занимает 80 бит, из-за чего пере
+менным этого типа сложно делать атомарные присваивания в 32-раз
+рядных программах. В любом случае, тип `real` предназначен для хране
+ния временных результатов высокой точности, а не для обмена данны
+ми, так что вряд ли у кого-то возникнет желание разделять значения
+этого типа.
+
+Для всех числовых типов и указателей на функции справедливо, что
+значения этих типов с квалификатором `shared` могут неявно преобразо
+вываться в значения без квалификатора и обратно. Преобразования
+указателей между `shared(T*)` и `shared(T)*` разрешены в обоих направле
+ниях. Арифметические операции на разделяемых числовых типах по
+зволяют выполнять примитивы из модуля `std.concurrency`.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+### 13.12.1. Последовательная целостность разделяемых данных
+
+Что касается видимости операций над разделяемыми данными между
+потоками, D предоставляет следующие гарантии:
+- порядок выполнения операций чтения и записи разделяемых дан
+ных в рамках одного потока соответствует порядку, определенному
+в исходном коде;
+- глобальный порядок выполнения операций чтения и записи разде
+ляемых данных представляет собой некоторое чередование опера
+ций чтения и записи, выполнение которых инициируется из разных
+потоков.
+
+Выбор этих инвариантов кажется вполне резонным, даже очевидным.
+И на самом деле такие гарантии довольно хорошо гармонируют с моде
+лью вытесняющей многозадачности, реализованной на однопроцессор
+ных системах.
+
+Тем не менее в контексте мультипроцессорных систем такие гарантии
+слишком строги. Проблема в следующем: для обеспечения этих гаран
+тий необходимо сделать так, чтобы результат выполнения любой опера
+ции записи был сразу же виден всем потокам. Единственный способ до
+биться этого – окружить обращения к разделяемым данным особыми
+машинными инструкциями (их называют *барьеры памяти*), которые
+обеспечивали бы соответствие порядка, в котором выполняются опера
+ции чтения и записи разделяемых данных, порядку обновления этих
+данных в глазах всех запущенных потоков. Присутствие замыслова
+тых иерархий кэшей значительно удорожает такую сериализацию.
+Кроме того, непоколебимая приверженность принципу последователь
+ной целостности заставляет отказаться от переупорядочивания опера
+ций – основы множества способов оптимизации на уровне компилято
+ра. В сочетании друг с другом эти два ограничения ведут к резкому за
+медлению – вплоть до одного порядка единиц измерения.
+
+Хорошая новость заключается в том, что такая потеря скорости имеет
+место лишь в отношении разделяемых данных, которые используются
+достаточно редко. В реальных ситуациях большинство данных не раз
+деляются, а потому нет необходимости, чтобы в их отношении соблю
+дался принцип последовательной целостности. Компилятор оптимизи
+рует код, используя неразделяемые данные на всю катушку, в полной
+уверенности, что другой поток никогда к ним не обратится, и относится
+с осторожностью лишь к разделяемым данным. Повсеместно использу
+емый и рекомендуемый прием для работы с разделяемыми данными –
+копировать значения разделяемых переменных в локальные рабочие
+копии потоков, работать с копиями и затем присваивать копии тем же
+разделяемым переменным.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.13. Синхронизация на основе блокировок через синхронизированные классы
+
+Традиционно популярный метод многопоточного программирования –
+*синхронизация на основе блокировок*. В соответствии с этим подходом
+разделяемые данные защищаются с помощью мьютексов – объектов
+синхронизации, обеспечивающих переход от параллельного к последо
+вательному исполнению фрагментов кода, которые или временно нару
+шают когерентность данных, или могут видеть эти временные наруше
+ния. Такие фрагменты кода называют *критическими участками*[^9].
+
+Корректность программы, основанной на блокировках, обеспечивается
+за счет ввода упорядоченного, последовательного доступа к разделяе
+мым данным. Поток, которому требуется обратиться к фрагменту раз
+деляемых данных, должен захватить (заблокировать) мьютекс, обрабо
+тать данные, а затем освободить (разблокировать) мьютекс. В любой за
+данный момент времени мьютексом может обладать только один поток,
+благодаря чему и обеспечивается переход к последовательному выпол
+нению: если захватить один и тот же мьютекс желают несколько пото
+ков, то «выигрывает» лишь один, а остальные скромно ожидают своей
+очереди. (Способ обслуживания очереди, то есть порядок очередности,
+играет важную роль и может довольно заметно сказываться на работе
+приложений и операционной системы.)
+
+По всей вероятности, «Здравствуй, мир!» многопоточного программи
+рования – это пример с банковским счетом: объект, доступный множе
+ству потоков, должен предоставить безопасный интерфейс для пополне
+ния счета и извлечения денежных средств со счета. Вот однопоточная,
+базовая версия программы, позволяющей выполнять эти действия:
+
+```d
+import std.contracts;
+
+// Однопоточный банковский счет
+class BankAccount
+{
+    private double _balance;
+    void deposit(double amount)
+    {
+        _balance += amount;
+    }
+    void withdraw(double amount)
+    {
+        enforce(_balance >= amount);
+        _balance -= amount;
+    }
+    @property double balance()
+    {
+        return _balance;
+    }
+}
+```
+
+В отсутствие потоков операции `+=` и `-=` слегка вводят в заблуждение: они
+«выглядят» как атомарные, но таковыми не являются – обе операции
+состоят из тройки простых операций «прочесть – изменить – записать».
+На самом деле, выражение `_balance += amount` кодируется как `_balance = _balance + amount`, а значит, процессор загружает `_balance` и `_amount` в соб
+ственную оперативную память (регистры или внутренний стек), скла
+дывает их, а затем переводит результат обратно в `_balance`.
+
+Незащищенные параллельные операции типа «прочесть – изменить –
+записать» становятся причиной некорректного поведения программы.
+Скажем, баланс вашего счета характеризует истинное выражение `_balance == 100.0`. Некоторый поток, запуск которого был спровоцирован
+требованием зачислить денежные средства по чеку, делает вызов `deposit(50)`. Сразу же после загрузки из памяти значения `100.0` выполнение
+этой операции прерывает другой поток, осуществляющий вызов `withdraw(2.5)`. (Это вы в кофейне на углу оплачиваете латте своей дебетовой
+картой.) Пусть ничто не вклинивается в обработку этого вызова, так что
+поток, запущенный из кофейни, удачно обновляет поле `_balance`, и оно
+принимает значение `97.5`. Однако это событие происходит совершенно
+без ведома депонирующего потока, который уже загрузил число `100`
+в регистр ЦПУ и все еще считает, что это верное количество. При вычис
+лении нового значения баланса вызов `deposit(50)` получает `150` и записы
+вает это число назад в переменную `_balance`. Это типичное *состояние гонки*. Поздравляю, вы получили бесплатный кофе (но остерегайтесь:
+книжные примеры с ошибками еще могут работать на вас, а готовый
+код с ошибками – нет). Для организации корректной синхронизации
+многие языки предоставляют специальное средство – тип `Mutex`, кото
+рый используется в программах, работающих с несколькими потоками
+на основе блокировок, для защиты доступа к `balance`:
+
+```d
+// Этот код написан не на D
+// Многопоточный банковский счет на языке с явным обращением к мьютексам
+class BankAccount
+{
+    private double _balance;
+    private Mutex _guard;
+    
+    void deposit(double amount)
+    {
+        _guard.lock();
+        _balance += amount;
+        _guard.unlock();
+    }
+
+    void withdraw(double amount)
+    {
+        _guard.lock();
+        try
+        {
+            enforce(_balance >= amount);
+            _balance -= amount;
+        }
+        finally
+        {
+            _guard.unlock();
+        }
+    }
+
+    @property double balance()
+    {
+        _guard.lock();
+        double result = _balance;
+        _guard.unlock();
+        return result;
+    }
+}
+```
+
+Все операции над `_balance` теперь защищены, поскольку для доступа
+к этому полю необходимо заполучить `_guard`. Может показаться, что при
+ставлять к `_balance` охранника в виде `_guard` излишне, так как значения
+типа `double` можно читать и записывать «в один присест», однако защита
+должна здесь присутствовать по причинам, скрытым многочисленны
+ми завесами майи. Вкратце, из-за сегодняшних агрессивно оптимизи
+рующих компиляторов и нестрогих моделей памяти *любое* обращение
+к разделяемым данным должно сопровождаться своего рода секрет
+ным соглашением между записывающим потоком, читающим потоком
+и оптимизирующим компилятором; одно неосторожное чтение разде
+ляемых данных – и вы оказываетесь в мире боли (хорошо, что D наме
+ренно запрещает такую «наготу»). Первая и наиболее очевидная при
+чина такого положения дел в том, что оптимизирующий компилятор,
+не замечая каких-либо попыток синхронизировать доступ к данным
+с вашей стороны, ощущает себя вправе оптимизировать код с обраще
+ниями к `_balance`, удерживая значение этого поля в регистре. Вторая
+причина в том, что во всех случаях, кроме самых тривиальных, компи
+лятор *и* ЦПУ ощущают себя вправе свободно переупорядочивать неза
+щищенные, не снабженные никаким дополнительным описанием обра
+щения к разделяемым данным, поскольку считают, что имеют дело
+с данными, принадлежащими лично одному потоку. (Почему? Да пото
+му что чаще всего так и бывает, оптимизация порождает код с самым
+высоким быстродействием, и в конце концов, почему должны страдать
+плебеи, а не избранные и достойные?) Это один из тех моментов, с помо
+щью которых современная многопоточность выражает свое пренебре
+жение к интуиции и сбивает с толку программистов, сведущих в клас
+сической многопоточности. Короче, чтобы обеспечить заключение сек
+ретного соглашения, потребуется обязательно синхронизировать обра
+щения к свойству `_balance`.
+
+Чтобы гарантировать корректное снятие блокировки с `Mutex` в условиях
+возникновения исключений и преждевременных возвратов управле
+ния, языки, в которых продолжительность жизни объектов контекстно
+ограничена (то есть деструкторы объектов вызываются на выходе из об
+ластей видимости этих объектов), определяют вспомогательный тип
+`Lock`, который устанавливает блок в конструкторе и снимает его в де
+структоре. Эта идея развилась в самостоятельную идиому, известную
+как *контекстное блокирование*. Приложение этой идиомы к клас
+су `BankAccount` выглядит так:
+
+```d
+// Версия C++: банковский счет, защищенный методом контекстного блокирования
+class BankAccount
+{
+private:
+    double _balance;
+    Mutex _guard;
+public:
+    void deposit(double amount)
+    {
+        Lock lock = Lock(_guard);
+        balance += amount;
+    }
+    void withdraw(double amount)
+    {
+        Lock lock = Lock(_guard);
+        enforce(_balance >= amount);
+        balance -= amount;
+    }
+    double balance()
+    {
+        Lock lock = Lock(_guard);
+        return _balance;
+    }
+}
+```
+
+Благодаря введению типа `Lock` код упрощается и повышается его кор
+ректность: ведь соблюдение парности операций установления и снятия
+блока теперь гарантировано, поскольку они выполняются автоматиче
+ски. Java, C# и другие языки еще сильнее упрощают работу с блоки
+ровками, встраивая `_guard` в объекты в качестве скрытого внутреннего
+элемента и приподнимая логику блокирования вверх, до уровня сигна
+туры метода. Наш пример, реализованный на Java, выглядел бы так:
+
+```d
+// Версия Java: банковский счет, защищенный методом контекстного
+// блокирования, автоматизированного с помощью инструкции synchronized
+class BankAccount
+{
+    private double _balance;
+    public synchronized void deposit(double amount)
+    {
+        _balance += amount;
+    }
+    public synchronized void withdraw(double amount)
+    {
+        enforce(_balance >= amount);
+        _balance -= amount;
+    }
+    public synchronized double balance()
+    {
+        return _balance;
+    }
+}
+```
+
+Соответствующий код на C# выглядит так же, за исключением того,
+что ключевое слово `synchronized` должно быть заменено на `[MethodImpl(MethodImplOptions.Synchronized)]`.
+
+Итак, вы только что узнали хорошую новость: небольшие программы,
+основанные на блокировках, легки для понимания и, кажется, неплохо
+работают. Плохая новость в том, что при большом масштабе очень слож
+но сопоставлять должным образом блокировки и данные, выбирать
+контекст и «калибр» блокирования и последовательно устанавливать
+блокировки, затрагивающие сразу несколько объектов (не говоря о том,
+что последнее может привести к тому, что взаимозаблокированные по
+токи, ожидая завершения работы друг друга, попадают в *тупик*). В ста
+рые добрые времена классического многопоточного программирования
+подобные проблемы весьма осложняли кодирование на основе блокиро
+вок; современная многопоточность (ориентированная на множество про
+цессоров, с нестрогими моделями памяти и дорогим разделением дан
+ных) поставила практику программирования с блокировками под удар. Тем не менее синхронизация на основе блокировок все еще полезна
+для реализации множества задумок.
+
+Для организации синхронизации с помощью блокировок D предостав
+ляет лишь ограниченные средства. Эти границы установлены намерен
+но: преимущество в том, что таким образом обеспечиваются серьезные
+гарантии. Что касается случая с `BankAccount`, версия D очень проста:
+
+```d
+// Версия D: банковский счет, реализованный с помощью синхронизированного класса
+synchronized class BankAccount
+{
+    private double _balance;
+    void deposit(double amount)
+    {
+        _balance += amount;
+    }
+    void withdraw(double amount)
+    {
+        enforce(_balance >= amount);
+        _balance -= amount;
+    }
+    @property double balance()
+    {
+        return _balance;
+    }
+}
+```
+
+D поднимает ключевое слово `synchronized` на один уровень выше, так
+чтобы оно применялось к целому классу[^10]. Благодаря этому маневру
+класс `BankAccount`, реализованный на D, получает возможность предос
+тавлять более серьезные гарантии: даже если бы вы пожелали совер
+шить ошибку, при таком синтаксисе нет способа оставить открытой
+хоть какую-нибудь дверь с черного хода для несинхронизированных
+обращений к `_balance`. Если бы в D позволялось смешивать использова
+ние синхронизированных и несинхронизированных методов в рамках
+одного класса, все обещания, данные синхронизированными метода
+ми, оказались бы нарушенными. На самом деле опыт с синхронизацией
+на уровне методов показал, что лучше всего синхронизировать или все
+методы, или ни один из них; классы двойного назначения приносят
+больше проблем, чем удобств.
+
+Объявляемый на уровне класса атрибут `synchronized` действует на объек
+ты типа `shared(BankAccount)` и автоматически превращает параллельное
+выполнение вызовов любых методов класса в последовательное. Кроме
+того, синхронизированные классы характеризуются возросшей строго
+стью проверок уровня защиты их внутренних элементов. Вспомните,
+в соответствии с разделом 11.1 обычные проверки уровня защиты в об
+щем случае позволяют обращаться к любым не общедоступным (`public`)
+внутренним элементам модуля любому коду внутри этого модуля. Толь
+ко не в случае синхронизированных классов – классы с атрибутом `synchronized` подчиняются следующим правилам:
+
+- объявлять общедоступные (`public`) данные и вовсе запрещено;
+- право доступа к защищенным (`protected`) внутренним элементам
+есть только у методов текущего класса и его потомков;
+- право доступа к закрытым (`private`) внутренним элементам есть толь
+ко у методов текущего класса.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.14. Типизация полей в синхронизированных классах
+
+В соответствии с правилом транзитивности для разделяемых (`shared`)
+объектов разделяемый объект класса распространяет квалификатор
+`shared` на свои поля. Очевидно, что атрибут `synchronized` привносит неко
+торый дополнительный закон и порядок, что отражается в нестрогой
+проверке типов полей внутри методов синхронизированных классов.
+Ключевое слово `synchronized` должно предоставлять серьезные гарантии,
+поэтому его присутствие своеобразно отражается на семантической про
+верке полей, в чем прослеживается настолько же своеобразная семанти
+ка самого атрибута `synchronized`.
+
+Защита синхронизированных методов от гонок *временна* и *локальна*.
+Свойство временности означает, что как только метод возвращает управ
+ление, поля от гонок больше не защищаются. Свойство локальности
+подразумевает, что `synchronized` обеспечивает защиту данных, встроен
+ных непосредственно в объект, но не данных, на которые объект ссыла
+ется косвенно (то есть через ссылки на классы, указатели или массивы).
+Рассмотрим каждое из этих свойств по очереди.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+### 13.14.1. Временная защита == нет утечкам
+
+Возможно, это не вполне очевидно, но «побочным эффектом» времен
+ной природы `synchronized` становится формирование следующего пра
+вила: ни один адрес поля не в состоянии «утечь» из синхронизирован
+ного кода. Если бы такое произошло, некоторый другой фрагмент кода
+получил бы право доступа к некоторым данным за пределами времен
+ной защиты, даруемой синхронизацией на уровне методов.
+
+Компилятор пресечет любые поползновения возвратить из метода ссыл
+ку или указатель на поле или передать значение поля по ссылке или по
+указателю в некоторую функцию. Покажем, в чем смысл этого прави
+ла, на следующем примере[^11]:
+
+```d
+double * nyukNyuk; // Обратите внимание: без shared
+
+void sneaky(ref double r) { nyukNyuk = &r; }
+
+synchronized class BankAccount
+{
+    private double _balance;
+    void fun()
+    {
+        nyukNyuk = &_balance; // Ошибка! (как и должно быть в этом случае)
+        sneaky(_balance);     // Ошибка! (как и должно быть в этом случае)
+    }
+}
+```
+
+В первой строке `fun` осуществляется попытка получить адрес `_balance`
+и присвоить его глобальной переменной. Если бы эта операция заверши
+лась успехом, гарантии системы типов превратились бы в ничто – с мо
+мента «утечки» адреса появилась бы возможность обращаться к разде
+ляемым данным через неразделяемое значение. Присваивание не прохо
+дит проверку типов. Вторая операция чуть более коварна в том смысле,
+что предпринимает попытку создать псевдоним более изощренным спо
+собом – через вызов функции, принимающей параметр по ссылке. Та
+кое тоже не проходит; передача значения с помощью `ref` фактически
+влечет получение адреса до совершения вызова. Операция получения
+адреса запрещена, так что и вызов завершается неудачей.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+### 13.14.2. Локальная защита == разделение хвостов
+
+Защита, которую предоставляет `synchronized`, обладает еще одним важ
+ным качеством – она локальна. Имеется в виду, что она не обязательно
+распространяется на какие-либо данные помимо непосредственных по
+лей объекта. Как только на горизонте появляются косвенности, гаран
+тия того, что потоки будут обращаться к данным по одному, практиче
+ски утрачивается. Если считать, что данные состоят из «головы» (часть,
+расположенная в физической памяти, которую занимает объект клас
+са `BankAccount`) и, возможно, «хвоста» (косвенно доступная память), то
+можно сказать, что синхронизированный класс в состоянии защитить
+лишь «голову» данных, в то время как «хвост» остается разделяемым
+(`shared`). По этой причине типизация полей синхронизированного (`synchronized`) класса внутри метода выполняется особым образом:
+
+- значения любых числовых типов не разделяются (у них нет хвоста),
+так что с ними можно обращаться как обычно (не применяется атри
+бут `shared`);
+- поля-массивы, тип которых объявлен как `T[]`, получают тип `shared(T)[]`; то есть голова (границы среза) не разделяется, а хвост (со
+держимое массива) остается разделяемым;
+- поля-указатели, тип которых объявлен как `T*`, получают тип `shared(T)*`; то есть голова (сам указатель) не разделяется, а хвост (дан
+ные, на которые указывает указатель) остается разделяемым;
+- поля-классы, тип которых объявлен как `T`, получают тип `shared(T)`.
+К классам можно обратиться лишь по ссылке (это делается автома
+тически), так что они представляют собой «сплошной хвост».
+
+Эти правила накладываются поверх правила о запрете «утечек», опи
+санного в предыдущем разделе. Прямое следствие такой совокупности
+правил: операции, затрагивающие непосредственные поля объекта,
+внутри метода можно свободно переупорядочивать и оптимизировать,
+как если бы разделение этих полей было временно остановлено – а имен
+но это и делает `synchronized`.
+
+Иногда один объект полностью владеет другим. Предположим, что
+класс `BankAccount` сохраняет все свои предыдущие транзакции в списке
+значений типа `double`:
+
+```d
+// Не синхронизируется и вообще понятия не имеет о потоках
+class List(T) {
+...
+void append(T value) {
+    ...
+}
+}
+// Ведет список транзакций
+synchronized class BankAccount
+{
+    private double _balance;
+    private List!double _transactions;
+
+    void deposit(double amount)
+    {
+        _balance += amount;
+        _transactions.append(amount);
+    }
+    
+    void withdraw(double amount)
+    {
+        enforce(_balance >= amount);
+        _balance -= amount;
+        _transactions.append(-amount);
+    }
+
+    @property double balance()
+    {
+        return _balance;
+    }
+}
+```
+
+Класс `List` не проектировался специально для разделения между пото
+ками, поэтому он не использует никакой механизм синхронизации, но
+к нему и на самом деле никогда не обращаются параллельно! Все обра
+щения к этому классу замурованы в объекте класса `BankAccount` и полно
+стью защищены, поскольку находятся внутри синхронизированных ме
+тодов. Если предполагать, что `List` не станет затевать никаких безумных
+проделок вроде сохранения некоторого внутреннего указателя в гло
+бальной переменной, такой код должен быть вполне приемлемым.
+
+К сожалению, это не так. В языке D код, подобный приведенному выше,
+не заработает никогда, поскольку вызов метода `append` применительно
+к объекту типа `shared(List!double)` некорректен. Одна из очевидных при
+чин отказа компилятора от такого кода в том, что компиляторы никому
+не верят на слово. Класс `List` может хорошо себя вести и все такое, но
+компилятору потребуется более веское доказательство того, что за его
+спиной не происходит никакое создание псевдонимов разделяемых дан
+ных. В теории компилятор мог бы пойти дальше и проверить определе
+ние класса `List`, однако `List`, в свою очередь, мог бы использовать другие
+компоненты, расположенные в других модулях, так что не успеете вы
+сказать «межпроцедурный анализ», как код «на обещаниях» начнет
+выходить из-под контроля.
+
+*Межпроцедурный анализ* – это техника, применяемая компиляторами
+и анализаторами программ для доказательства справедливости предпо
+ложений о программе с помощью одновременного рассмотрения сразу
+нескольких функций. Такие алгоритмы анализа обычно обладают низ
+кой скоростью, начинают хуже работать с ростом программы и являют
+ся заклятыми врагами раздельной компиляции. Некоторые системы
+используют межпроцедурный анализ, но большинство современных
+языков (включая D) выполняют все проверки типов, не прибегая к этой
+технике.
+
+Альтернативное решение проблемы с подобъектом-собственностью –
+ввести новые квалификаторы, которые бы описывали отношения владе
+ния, такие как «класс `BankAccount` является владельцем своего внутрен
+него элемента `_transactions`, следовательно, мьютекс `BankAccount` также
+обеспечивает последовательное выполнение операций над `_transactions`». При верном расположении таких примечаний компилятор смог бы
+получить подтверждение того, что объект `_transactions` полностью ин
+капсулирован внутри `BankAccount`, а потому безопасен в использовании,
+и к нему можно обращаться, не беспокоясь о неуместном разделении.
+Системы и языки, работающие по такому принципу, уже
+были представлены, однако в настоящий момент они погоды не делают.
+Ввод явного указания имеющихся отношений владения свидетельству
+ет о появлении в языке и компиляторе значительных сложностей. Учи
+тывая, что в настоящее время синхронизация на основе блокировок
+борется за существование, D поостерегся усиливать поддержку этой
+ущербной техники программирования. Не исключено, что это решение
+еще будет пересмотрено (для D были предложены системы моделирова
+ния отношений владения), но на настоящий момент, чтобы реализо
+вать некоторые проектные решения, основанные на блокировках, при
+ходится, как объясняется далее, переступать границы системы типов.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+### 13.14.3. Принудительные идентичные мьютексы
+
+D позволяет сделать динамически то, что система типов не в состоянии
+гарантировать статически: отношение «владелец/собственность» в кон
+тексте блокирования. Для этого предлагается следующая глобально
+доступная базовая функция:
+
+```d
+// Внутри object.d
+setSameMutex(shared Object ownee, shared Object owner);
+```
+
+Объект `obj` некоторого класса может сделать вызов `obj.setSameMutex(owner)`[^12], и в результате вместо текущего объекта синхронизации `obj` нач
+нет использовать тот же объект синхронизации, что и объект `owner`. Та
+ким способом можно гарантировать, что при блокировке объекта `owner`
+блокируется и объект `obj`. Посмотрим, как это сработает применитель
+но к нашим подопытным классам `BankAccount` и `List`.
+
+```d
+// В курсе о существовании потоков
+synchronized class List(T)
+{
+    ...
+    void append(T value)
+    {
+        ...
+    }
+}
+
+// Ведет список транзакций
+synchronized class BankAccount
+{
+    private double _balance;
+    private List!double _transactions;
+    
+    this()
+    {
+        // Счет владеет списком
+        setSameMutex(_transactions, this);
+    }
+    ...
+}
+```
+
+Необходимое условие работы такой схемы – синхронизация обращений
+к `List` (объекту-собственности). Если бы к объекту `_transactions` приме
+нялись лишь обычные правила для полей, впоследствии при выполне
+нии над ним операций он бы просто заблокировался в соответствии
+с этими правилами. Но на самом деле при обращении к `_transactions`
+происходит кое-что необычное: осуществляется явный захват мьютек
+са объекта типа `BankAccount`. При такой схеме мы получаем довольный
+компилятор: он думает, что каждый объект блокируется по отдельно
+сти. Довольна и программа: на самом деле единственный мьютекс кон
+тролирует как объект типа `BankAccount`, так и подобъект типа List. За
+хват мьютекса поля `_transactions` – это в действительности захват уже
+заблокированного мьютекса объекта `this`. К счастью, такой рекурсив
+ный захват уже заблокированного, не запрашиваемого другими пото
+ками мьютекса обходится относительно дешево, так что представлен
+ный в примере код корректен и не снижает производительность про
+граммы за счет частого блокирования.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+### 13.14.4. Фильм ужасов: приведение от shared
+
+Продолжим работать с предыдущим примером. Если вы абсолютно уве
+рены в том, что желаете возвести список `_transactions` в ранг святой част
+ной собственности объекта типа `BankAccount`, то можете избавиться от
+`shared` и использовать `_transactions` без учета потоков:
+
+```d
+// Не синхронизируется и вообще понятия не имеет о потоках
+class List(T)
+{
+    ...
+    void append(T value)
+    {
+        ...
+    }
+}
+
+synchronized class BankAccount
+{
+    private double _balance;
+    private List!double _transactions;
+
+    void deposit(double amount)
+    {
+        _balance += amount;
+        (cast(List!double) _transactions).append(amount);
+    }
+
+    void withdraw(double amount)
+    {
+        enforce(_balance >= amount);
+        _balance -= amount;
+        (cast(List!double) _transactions).append(-amount);
+    }
+
+    @property double balance()
+    {
+        return _balance;
+    }
+}
+```
+
+На этот раз код с несинхронизированным классом `List` и компилирует
+ся, и запускается. Однако есть одно «но»: теперь корректность основан
+ной на блокировках дисциплины в программе гарантируете вы, а не
+система типов языка, так что ваше положение не намного лучше, чем
+при использовании языков с разделением данных по умолчанию. Пре
+имущество, которым вы все же можете наслаждаться, состоит в том,
+что приведения типов локализованы, а значит, их легко находить, то
+есть тщательно рассмотреть обращения к `cast` на предмет ошибок – не
+проблема.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.15. Взаимоблокировки и инструкция synchronized
+
+Если пример с банковским счетом – это «Здравствуй, мир!» программ,
+использующих потоки, то пример с переводом средств со счета на счет,
+надо полагать, – соответствующее (но более мрачное) введение в пробле
+му межпоточных взаимоблокировок. Условия для задачи с переводом
+средств формулируются так: пусть даны два объекта типа `BankAccount`
+(скажем, `checking` и `savings`); требуется определить атомарный перевод
+некоторого количества денежных средств с одного счета на другой.
+
+Типичное наивное решение выглядит так:
+
+```d
+// Перевод средств. Версия 1: не атомарная
+void transfer(shared BankAccount source, shared BankAccount target, double amount)
+{
+    source.withdraw(amount);
+    target.deposit(amount);
+}
+```
+
+Тем не менее эта версия не атомарна; в промежутке между двумя вызо
+вами в теле `transfer` деньги отсутствуют на обоих счетах. Если точно
+в этот момент времени другой поток выполнит функцию `inspectForAuditing`, обстановка может обостриться.
+
+Чтобы сделать операцию перевода средств атомарной, потребуется осу
+ществить захват скрытых мьютексов двух объектов за пределами их
+методов, в начале функции `transfer`. Это можно организовать с помо
+щью инструкций `synchronized`:
+
+```d
+// Перевод средств. Версия 2: ГЕНЕРАТОР ПРОБЛЕМ
+void transfer(shared BankAccount source, shared BankAccount target, double amount)
+{
+    synchronized (source)
+    {
+        synchronized (target)
+        {
+            source.withdraw(amount);
+            target.deposit(amount);
+        }
+    }
+}
+```
+
+Инструкция `synchronized` захватывает скрытый мьютекс объекта на
+время выполнения своего тела. За счет этого вызванные методы данно
+го объекта имеют преимущество уже установленного блока.
+
+Проблема со второй версией функции `transfer` в том, что она предраспо
+ложена к взаимоблокировкам (тупикам): если два потока попытаются
+выполнить операции перевода между одними и теми же счетами, но
+*в противоположных направлениях*, то эти потоки могут заблокировать
+друг друга навсегда. Поток, пытающийся перевести деньги со счета `checking` на счет `savings`, блокирует счет `checking`, а другой поток, пытаю
+щийся перевести деньги со счета `savings` на счет `checking`, совершенно
+симметрично умудряется заблокировать счет `savings`. Этот момент ха
+рактеризуется тем, что каждый из потоков удерживает свой мьютекс,
+но для продолжения работы каждому из потоков необходим мьютекс
+другого потока. К согласию такие потоки не придут никогда.
+
+Решить эту проблему позволяет инструкция `synchronized` с *двумя* аргу
+ментами:
+
+```d
+// Перевод средств. Версия 3: верная
+void transfer(shared BankAccount source, shared BankAccount target, double amount)
+{
+    synchronized (source, target)
+    {
+        source.withdraw(amount);
+        target.deposit(amount);
+    }
+}
+```
+
+Синхронизация обращений сразу к нескольким объектам с помощью
+одной и той же инструкции `synchronized` и последовательная синхрони
+зация каждого из этих объектов – разные вещи. Сгенерированный код
+захватывает мьютексы всегда в том же порядке во всех потоках, невзи
+рая на синтаксический порядок, в котором вы укажете объекты син
+хронизации. Таким образом, взаимоблокировки предотвращаются.
+
+В случае эталонной реализации компилятора истинный порядок уста
+новки блокировок соответствует порядку увеличения адресов объектов.
+Но здесь подходит любой порядок, лишь бы он учитывал все объекты.
+
+Инструкция `synchronized` с несколькими аргументами помогает, но, к со
+жалению, не всегда. В общем случае действия, вызывающие взаимобло
+кировку, могут быть «территориально распределены»: один мьютекс за
+хватывается в одной функции, затем другой – в другой и так далее до
+тех пор, пока круг не замкнется и не возникнет тупик. Однако `synchronized` со множеством аргументов дает дополнительные знания о пробле
+ме и способствует написанию корректного кода с блочным захватом
+мьютексов.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.16. Кодирование без блокировок с помощью разделяемых классов
+
+Теория синхронизации, основанной на блокировках, сформировалась
+в 1960-х. Но уже к 1972 году исследователи стали искать пути ис
+ключения из многопоточных программ медленных, неуклюжих мью
+тексов, насколько это возможно. Например, операции присваивания
+с некоторыми типами можно было выполнять атомарно, и программи
+сты осознали, что охранять такие присваивания с помощью захвата
+мьютексов нет нужды. Кроме того, некоторые процессоры стали выпол
+нять транзакционно и более сложные операции, такие как атомарное
+увеличение на единицу или «проверить-и-установить». Около тридцати
+лет спустя, в 1990 году, появился луч надежды, который выглядел впол
+не определенно: казалось, должна отыскаться какая-то хитрая комби
+нация регистров для чтения и записи, позволяющая избежать тирании
+блокировок. И в этот момент появилась полная плодотворных идей ра
+бота, которая положила конец исследованиям в этом направлении,
+предложив другое.
+
+Статья Мориса Херлихи «Синхронизация без ожидания» (1991) озна
+меновала мощный рывок в развитии параллельных вычислений. До это
+го разработчикам и аппаратного, и программного обеспечения было оди
+наково неясно, с какими примитивами синхронизации лучше всего ра
+ботать. Например, процессор, который поддерживает атомарные опера
+ции чтения и записи значений типа `int`, интуитивно могли счесть менее
+мощным, чем тот, который помимо названных операций поддерживает
+еще и атомарную операцию `+=`, а третий, который вдобавок предостав
+ляет атомарную операцию `*=`, казался еще мощнее. В общем, чем боль
+ше атомарных примитивов в распоряжении пользователя, тем лучше.
+
+Херлихи разгромил эту теорию, в частности показав фактическую бес
+полезность казавшихся мощными примитивов синхронизации, таких
+как «проверить-и-установить», «получить-и-сложить» и даже глобаль
+ная разделяемая очередь типа FIFO. В свете этих *парадоксов* мгновенно
+развеялась иллюзия, что из подобных механизмов можно добыть маги
+ческий эликсир для параллельных вычислений. К счастью, помимо по
+лучения этих неутешительных результатов Херлихи доказал справед
+ливость *выводов об универсальности*: определенные примитивы син
+хронизации могут теоретически синхронизировать любое количество
+параллельно выполняющихся потоков. Поразительно, но реализовать
+«хорошие» примитивы ничуть не труднее, чем «плохие», причем на не
+вооруженный глаз они не кажутся особенно мощными. Из всех полез
+ных примитивов синхронизации прижился лишь один, известный как
+сравнение с обменом (compare-and-swap). Сегодня этот примитив реали
+зует фактически любой процессор. Семантика операции сравнения с об
+меном:
+
+```d
+// Эта функция выполняется атомарно
+bool cas(T)(shared(T) * here, shared(T) ifThis, shared(T) writeThis)
+{
+    if (*here == ifThis)
+    {
+        *here = writeThis;
+        return true;
+    }
+    return false;
+}
+```
+
+В переводе на обычный язык операция `cas` атомарно сравнивает данные
+в памяти по заданному адресу с заданным значением и, если значение
+в памяти равно переданному явно, сохраняет новое значение; в против
+ном случае не делает ничего. Результат операции сообщает, выполня
+лось ли сохранение. Операция `cas` целиком атомарна и должна предос
+тавляться в качестве примитива. Множество возможных типов `T` огра
+ничено целыми числами размером в слово той машины, где будет вы
+полняться код (то есть 32 и 64 бита). Все больше машин предоставляют
+операцию *сравнения с обменом для аргументов размером в двойное слово* (*double-word compare-and-swap*), иногда ее называют `cas2`. Операция
+`cas2` автоматически обрабатывает 64-битные данные на 32-разрядных
+машинах и 128-битные данные на 64-разрядных машинах. Ввиду того
+что все больше современных машин поддерживают `cas2`, D предостав
+ляет операцию сравнения с обменом для аргументов размером в двой
+ное слово под тем же именем (`cas`), под которым фигурирует и перегру
+женная внутренняя функция. Так что в D можно применять операцию
+`cas` к значениям типов `int`, `long`, `float`, `double`, любых массивов, любых
+указателей и любых ссылок на классы.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+### 13.16.1. Разделяемые классы
+
+Херлиховские доказательства универсальности спровоцировали появ
+ление и рост популярности множества структур данных и алгоритмов
+в духе нарождающегося «программирования на основе `cas»`. Но есть
+один нюанс: хотя реализация на основе `cas` и возможна теоретически для
+любой задачи синхронизации, но никто не сказал, что это легко. Опреде
+ление структур данных и алгоритмов на основе `cas` и особенно доказа
+тельство корректности их работы – дело нелегкое. К счастью, однажды
+определив и инкапсулировав такую сущность, ее можно повторно ис
+пользовать для решения самых разных задач.
+
+Чтобы ощутить благодать программирования без блокировок на основе
+`cas`, воспользуйтесь атрибутом `shared` применительно к классу или
+структуре:
+
+```d
+shared struct LockFreeStruct
+{
+    ...
+}
+
+shared class LockFreeClass
+{
+    ...
+}
+```
+
+Обычные правила относительно транзитивности в силе: разделяемость
+распространяется на поля структуры или класса, а методы не предо
+ставляют никакой особой защиты. Все, на что вы можете рассчиты
+вать, – это атомарные присваивания, вызовы `cas`, уверенность в том, что
+ни компилятор, ни машина не переупорядочат операции, и собственная
+безграничная самоуверенность. Однако остерегайтесь: если написание
+кода – ходьба, а передача сообщений – бег трусцой, то программирова
+ние без блокировок – Олимпийские игры, не меньше.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+### 13.16.2. Пара структур без блокировок
+
+Для разминки реализуем стек без блокировок. Основная идея проста:
+стек моделируется с помощью односвязного списка, операции вставки
+и удаления выполняются для элементов, расположенных в начале это
+го списка:
+
+```d
+shared struct Stack(T)
+{
+    private shared struct Node
+    {
+        T _payload;
+        Node * _next;
+    }
+
+    private Node * _root;
+
+    void push(T value)
+    {
+        auto n = new Node(value);
+        shared(Node)* oldRoot;
+
+        do
+        {
+            oldRoot = _root;
+            n._next = oldRoot;
+        } while (!cas(&_root, oldRoot, n));
+    }
+
+    shared(T)* pop()
+    {
+        typeof(return) result;
+        shared(Node)* oldRoot;
+
+        do
+        {
+            oldRoot = _root;
+            if (!oldRoot) return null;
+            result = & oldRoot._payload;
+        } while (!cas(&_root, oldRoot, oldRoot._next));
+
+        return result;
+    }
+}
+```
+
+`Stack` является разделяемой структурой, отсюда прямое следствие: внут
+ри нее практически все тоже разделяется. Внутренний тип `Node` – клас
+сическая структура «полезные данные + указатель», а сам тип `Stack`
+хранит указатель на начало списка.
+
+Циклы `do`/`while` в теле обеих функций, реализующих базовые операции
+над стеком, могут показаться странноватыми, но в них нет ничего осо
+бенного; медленно, но верно они прокладывают глубокую борозду в ко
+ре головного мозга каждого будущего эксперта в `cas`-программирова
+нии. Функция `push` работает так: сначала создается новый узел, в кото
+ром будет сохранено новое значение. Затем в цикле переменной `_root`
+присваивается указатель на новый узел, но *только* если тем временем
+никакой другой поток не изменил ее! Вполне возможно, что другой по
+ток также выполнил какую-то операцию со стеком, так что функции
+`push` нужно удостовериться в том, что указатель на начало стека, кото
+рому, как предполагается, соответствует значение переменной `oldRoot`,
+не изменился за время подготовки нового узла.
+
+Метод `pop` возвращает результат не по значению, а через указатель. При
+чина в том, что `pop` может обнаружить очередь пустой, ведь это не явля
+ется нештатной ситуацией (как было бы, будь перед нами стек, предна
+значенный лишь для последовательных вычислений). В случае разде
+ляемого стека проверка наличия элемента, его удаление и возврат со
+ставляют одну согласованную операцию. За исключением возвращения
+результата, функция `pop` по реализации напоминает функцию `push`: за
+мена `_root` выполняется с большой осторожностью, так чтобы никакой
+другой поток не изменил значение этой переменной, пока извлекаются
+полезные данные. В конце цикла извлеченное значение отсутствует
+в стеке и может быть спокойно возвращено инициатору вызова.
+
+Если реализация класса `Stack` не показалась вам такой уж сложной,
+возьмемся за реализацию более богатого односвязного интерфейса;
+в конце концов большая часть инфраструктуры уже выстроена в рам
+ках класса `Stack`.
+
+К сожалению, в случае со списком все угрожает быть гораздо сложнее.
+Насколько сложнее? Нечеловечески сложнее. Одна из фундаменталь
+ных проблем – вставка и удаление узлов в произвольных позициях спи
+ска. Предположим, есть список значений типа `int`, а в нем есть узел
+с числом `5`, за которым следует узел с числом `10`, и требуется удалить
+узел с числом `5`. Тут проблем нет – просто пустите в ход волшебную опе
+рацию cas, чтобы нацелить указатель `_root` на узел с числом `10`. Пробле
+ма в том, что в то же самое время другой поток может вставлять новый
+узел прямо после узла с числом `5` – узел, который будет безвозвратно
+потерян, поскольку `_root` ничего не знает о нем.
+
+В литературе представлено несколько возможных решений; ни одно из
+них нельзя назвать тривиально простым. Реализация, представленная
+ниже, впервые была предложена Тимоти Харрисом в его работе с мно
+гообещающим названием «Прагматическая реализация неблокирую
+щих односвязных списков». Эта реализация немного шероховата,
+поскольку ее логика основана на установке младшего неиспользуемого
+бита указателя `_next`. Идея состоит в том, чтобы сначала сделать на этом
+указателе пометку «логически удален» (обнулив его бит), а затем на вто
+ром шаге вырезать соответствующий узел целиком.
+
+```d
+shared struct SharedList(T)
+{
+    shared struct Node
+    {
+        private T _payload;
+        private Node * _next;
+
+        @property shared(Node)* next()
+        {
+            return clearlsb(_next);
+        }
+
+        bool removeAfter()
+        {
+            shared(Node)* thisNext, afterNext;
+            // Шаг 1: сбросить младший бит поля _next узла, предназначенного для удаления            
+            do
+            {
+                thisNext = next;
+                if (!thisNext) return false;
+                afterNext = thisNext.next;
+            } while (!cas(&thisNext._next, afterNext, setlsb(afterNext)));
+
+            // Шаг 2: вырезать узел, предназначенный для удаления
+            if (!cas(&_next, thisNext, afterNext))
+            {
+                afterNext = thisNext._next;
+                while (!haslsb(afterNext))
+                {
+                    thisNext._next = thisNext._next.next;
+                }
+                _next = afterNext;
+            }
+        }
+
+        void insertAfter(T value)
+        {
+            auto newNode = new Node(value);
+            for (;;)
+            {
+                // Попытка найти место вставки
+                auto n = _next;
+                while (n && haslsb(n))
+                {
+                    n = n._next;
+                }
+                // Найдено возможное место вставки, попытка вставки
+                auto afterN = n._next;
+                newNode._next = afterN;
+                if (cas(&n._next, afterN, newNode))
+                {
+                    break;
+                }
+            }
+        }
+    }
+
+    private Node * _root;
+
+    void pushFront(T value)
+    {
+        ... // То же, что Stack.push
+    }
+
+    shared(T)* popFront()
+    {
+        ... // То же, что Stack.pop
+    }
+}
+```
+
+Реализация непростая, но ее можно понять, если, разбирая код, дер
+жать в голове пару инвариантов. Во-первых, для логически удаленных
+узлов (то есть объектов типа `Node` с полем `_next`, младший бит которого
+сброшен) вполне нормально повисеть какое-то время среди обычных уз
+лов. Во-вторых, узел никогда не вставляют после удаленного узла. Та
+ким образом, состояние списка остается корректным, несмотря на то,
+что узлы могут появляться и исчезать в любой момент времени.
+
+Реализации функций `clearlsb`, `setlsb` и `haslsb` грубы, насколько это воз
+можно; например:
+
+```d
+T* setlsb(T)(T* p)
+{
+    return cast(T*) (cast(size_t) p | 1);
+}
+```
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.17. Статические конструкторы и потоки[^13]
+
+В одной из предыдущих глав была описана конструкция `static this()`,
+предназначенная для инициализации статических данных модулей
+и классов:
+
+```d
+module counters;int counter = 0;
+static this()
+{
+    counter++;
+}
+```
+
+Как уже говорилось, у каждого потока есть локальная копия перемен
+ной `counter`. Каждый новый поток получает копию этой переменной,
+и при создании этого потока запускается статический конструктор.
+
+```d
+import std.concurrency, std.stdio;
+
+int counter = 0;
+
+static this()
+{
+    counter++;
+    writeln("Статический конструктор: counter = ", counter);
+}
+
+void main()
+{
+    writeln("Основной поток");
+    spawn(&fun);
+}
+
+void fun()
+{
+    writeln("Дочерний поток");
+}
+```
+
+Запустив этот код, получим вывод:
+
+```
+Статический конструктор: counter = 1
+Основной поток
+Статический конструктор: counter = 1
+Дочерний поток
+```
+
+Объявить статический конструктор, исполняемый один раз при запус
+ке программы и предназначенный для инициализации разделяемых
+данных, можно с помощью конструкции `shared static this()`, а объя
+вить разделяемый деструктор – с помощью конструкции `shared static ~this()`:
+
+```d
+import std.concurrency, std.stdio;
+
+shared int counter = 0;
+
+shared static this()
+{
+    counter++;
+    writeln("Статический конструктор: counter = ", counter);
+}
+
+void main()
+{
+    writeln("Основной поток");
+    spawn(&fun);
+}
+
+void fun()
+{
+    writeln("Дочерний поток");
+}
+```
+
+В этом случае конструктор будет запущен только один раз:
+
+```
+Статический конструктор: counter = 1
+Основной поток
+Дочерний поток
+```
+
+Разделяемыми могут быть не только конструкторы и деструкторы мо
+дуля, но и статические конструкторы и деструкторы класса. Порядок
+выполнения разделяемых статических конструкторов и деструкторов
+определяется теми же правилами, что и порядок выполнения локаль
+ных статических конструкторов и деструкторов.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+## 13.18. Итоги
+
+Реализация функции `setlsb`, грязная и с потеками масла на стыках,
+была бы подходящим заключением для главы, которая началась со
+строгой красоты обмена сообщениями и постепенно спустилась в под
+земный мир разделения данных.
+
+D предлагает широкий спектр средств для работы с потоками. Наибо
+лее предпочтительный механизм для большинства приложений на со
+временных машинах – определение протоколов на основе обмена сооб
+щениями. При таком выборе может здорово пригодиться неизменяемое
+разделение. Отличный совет для тех, кто хочет проектировать надеж
+ные, масштабируемые приложения, использующие параллельные вы
+числения, – организовать взаимодействие между потоками по методу
+обмена сообщениями.
+
+Если требуется определить синхронизацию на основе взаимоисключе
+ния, это можно осуществить с помощью синхронизированных классов.
+Но предупреждаю: по сравнению с другими языками, поддержка про
+граммирования на основе блокировок в D ограничена, и на это есть ос
+нования.
+
+Если требуется простое разделение данных, можно воспользоваться раз
+деляемыми (`shared`) значениями. D гарантирует, что операции с разде
+ляемыми значениями выполняются в порядке, определенном в вашем
+коде, и не провоцируют парадоксы видимости и низкоуровневые гонки.
+
+Наконец, если вам наскучили такие аттракционы, как банджи-джам
+пинг, укрощение крокодилов и прогулки по раскаленным углям, вы бу
+дете счастливы узнать, что существует программирование без блокиро
+вок и что вы можете заниматься этим в D, используя разделяемые
+структуры и классы.
+
+[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления)
+
+[^1]: Число транзисторов на кристалл будет увеличиваться вдвое каждые 24 месяца. – *Прим. пер.*
+[^2]: Далее речь идет о параллельных вычислениях в целом и не рассматриваются распараллеливание операций над векторами и другие специализированные параллельные функции ядра.
+[^3]: Что иронично, поскольку во времена классической многопоточности разделение памяти было быстрее, а обмен сообщениями – медленнее.
+[^4]: Даже заголовок раздела был изменен с «Потоки» на «Параллельные вычисления», чтобы подчеркнуть, что потоки – это не что иное, как одна из моделей параллельных вычислений.
+[^5]: Процессы языка Erlang отличаются от процессов ОС.
+[^6]: Подразумевалось обратное от «насыплем соль на рану».
+[^7]: Речь идет о самом процессе программирования: правила, соблюдение которых компилятор гарантировать не может, люди рано или поздно начнут нарушать (с плачевными последствиями). – *Прим. науч. ред.*
+[^8]: Кстати, воспользовавшись квалификатором `const`, вы сможете делиться бумажником, зная при этом, что деньги в нем защищены от воров. Стоит лишь ввести тип `shared(const(Money)*)`.
+[^9]: Возможна путаница из-за того, что Windows использует термин «критический участок» для обозначения легковесных объектов мьютексов, защищающих критические участки, а «мьютекс» – для более массивных мьютексов, с помощью которых организуется передача данных между процессами.
+[^10]: Впрочем, D разрешает объявлять синхронизированными отдельные методы класса (в том числе статические). – *Прим. науч. ред.*
+[^11]: nyukNyuk («няк-няк») – «фирменный» смех комика Керли Ховарда. – *Прим. пер.*
+[^12]: На момент выхода книги возможность вызова функций как псевдочленов (см. раздел 5.9) не была реализована полностью, и вместо кода `obj.setSameMutex(owner)` нужно было писать `setSameMutex(obj, owner)`. Возможно, все уже изменилось. – *Прим. науч. ред.*
+[^13]: Описание этой части языка не было включено в оригинал книги, но поскольку эта возможность присутствует в текущих реализациях языка, мы добавили ее описание в перевод. – *Прим. науч. ред.*