From 4d574460573e4b4bd68b4d11f518746bd999a332 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 5 Mar 2023 15:19:51 +0300 Subject: [PATCH] =?UTF-8?q?=D0=93=D0=BB=D0=B0=D0=B2=D0=B0=2013=20=D0=B3?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D0=B2=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 13-параллельные-вычисления/README.md | 1825 ++++---------------------- 1 file changed, 253 insertions(+), 1572 deletions(-) diff --git a/13-параллельные-вычисления/README.md b/13-параллельные-вычисления/README.md index 5edaba3..76ebda4 100644 --- a/13-параллельные-вычисления/README.md +++ b/13-параллельные-вычисления/README.md @@ -1,504 +1,134 @@ # 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. Итоги]() +- [13.1. Революция в области параллельных вычислений](#13-1-революция-в-области-параллельных-вычислений) +- [13.2. Краткая история механизмов разделения данных](#13-2-краткая-история-механизмов-разделения-данных) +- [13.3. Смотри, мам, никакого разделения (по умолчанию)](#13-3-смотри-мам-никакого-разделения-по-умолчанию) +- [13.4. Запускаем поток](#13-4-запускаем-поток) + - [13.4.1. Неизменяемое разделение](#13-4-1-неизменяемое-разделение) +- [13.5. Обмен сообщениями между потоками](#13-5-обмен-сообщениями-между-потоками) +- [13.6. Сопоставление по шаблону с помощью receive](#13-6-сопоставление-по-шаблону-с-помощью-receive) + - [13.6.1. Первое совпадение](#13-6-1-первое-совпадение) + - [13.6.2. Соответствие любому сообщению](#13-6-2-соответствие-любому-сообщению) +- [13.7. Копирование файлов – с выкрутасом](#13-7-копирование-файлов-с-выкрутасом) +- [13.8. Останов потока](#13-8-останов-потока) +- [13.9. Передача нештатных сообщений](#13-9-передача-нештатных-сообщений) +- [13.10. Переполнение почтового ящика](#13-10-переполнение-почтового-ящика) +- [13.11. Квалификатор типа shared](#13-11-квалификатор-типа-shared) + - [13.11.1. Сюжет усложняется: квалификатор shared транзитивен](#13-11-1-сюжет-усложняется-квалификатор-shared-транзитивен) +- [13.12. Операции с разделяемыми данными и их применение](#13-12-операции-с-разделяемыми-данными-и-их-применение) + - [13.12.1. Последовательная целостность разделяемых данных](#13-12-1-последовательная-целостность-разделяемых-данных) +- [13.13. Синхронизация на основе блокировок через синхронизированные классы](#13-13-синхронизация-на-основе-блокировок-через-синхронизированные-классы) +- [13.14. Типизация полей в синхронизированных классах](#13-14-типизация-полей-в-синхронизированных-классах) + - [13.14.1. Временная защита == нет утечкам](#13-14-1-временная-защита-нет-утечкам) + - [13.14.2. Локальная защита == разделение хвостов](#13-14-2-локальная-защита-разделение-хвостов) + - [13.14.3. Принудительные идентичные мьютексы](#13-14-3-принудительные-идентичные-мьютексы) + - [13.14.4. Фильм ужасов: приведение от shared](#13-14-4-фильм-ужасов-приведение-от-shared) +- [13.15. Взаимоблокировки и инструкция synchronized](#13-15-взаимоблокировки-и-инструкция-synchronized) +- [13.16. Кодирование без блокировок с помощью разделяемых классов](#13-16-кодирование-без-блокировок-с-помощью-разделяемых-классов) + - [13.16.1. Разделяемые классы](#13-16-1-разделяемые-классы) + - [13.16.2. Пара структур без блокировок](#13-16-2-пара-структур-без-блокировок) +- [13.17. Статические конструкторы и потоки](#13-17-статические-конструкторы-и-потоки-13) +- [13.18. Итоги](#13-18-итоги) -Благодаря сложившейся обстановке в индустрии аппаратного обеспече -ния качественно изменился способ доступа к вычислительным ресур -сам, которые, в свою очередь, требуют основательного пересмотра тех -ники вычислений и применяемых языковых абстракций. Сегодня ши -роко распространены параллельные вычисления, и программное обес -печение должно научиться извлекать из этого пользу. +Благодаря сложившейся обстановке в индустрии аппаратного обеспечения качественно изменился способ доступа к вычислительным ресурсам, которые, в свою очередь, требуют основательного пересмотра техники вычислений и применяемых языковых абстракций. Сегодня широко распространены параллельные вычисления, и программное обеспечение должно научиться извлекать из этого пользу. -Несмотря на то что индустрия программного обеспечения в целом еще -не выработала окончательные ответы на вопросы, поставленные рево -люцией в области параллельных вычислений, молодость D позволила -его создателям, не связанным ни устаревшими концепциями прошло -го, ни огромным наследством базового кода, принять компетентные ре -шения относительно параллелизма. Главное отличие подхода D от стан -дарта поддерживающих параллелизм императивных языков – в том, -что он не поощряет разделение данных между потоками; по умолчанию -параллельные потоки фактически изолированы друг от друга с помо -щью механизмов языка. Разделение данных разрешено, но лишь в огра -ниченной управляемой форме, чтобы компилятор мог предоставлять -основательные глобальные гарантии. +Несмотря на то что индустрия программного обеспечения в целом еще не выработала окончательные ответы на вопросы, поставленные революцией в области параллельных вычислений, молодость D позволила его создателям, не связанным ни устаревшими концепциями прошлого, ни огромным наследством базового кода, принять компетентные решения относительно параллелизма. Главное отличие подхода D от стандарта поддерживающих параллелизм императивных языков – в том, что он не поощряет разделение данных между потоками; по умолчанию параллельные потоки фактически изолированы друг от друга с помощью механизмов языка. Разделение данных разрешено, но лишь в ограниченной управляемой форме, чтобы компилятор мог предоставлять основательные глобальные гарантии. -В то же время D, оставаясь в душе языком для системного программи -рования, разрешает применять ряд низкоуровневых, неконтролируе -мых механизмов параллельных вычислений. (При этом в безопасных -программах некоторые из этих механизмов использовать запрещено.) +В то же время D, оставаясь в душе языком для системного программирования, разрешает применять ряд низкоуровневых, неконтролируемых механизмов параллельных вычислений. (При этом в безопасных программах некоторые из этих механизмов использовать запрещено.) Вот краткий обзор уровней параллелизма, предлагаемых языком D: -- Передовой подход к параллельным вычислениям заключается в ис -пользовании изолированных потоков или процессов, взаимодейст -вующих с помощью сообщений. Эта парадигма, называемая *обменом сообщениями* (*message passing*), позволяет создавать безопасные -модульные программы, легкие для понимания и сопровождения. -Обмен сообщениями успешно применяется в разнообразных языках -и библиотеках. Раньше обмен сообщениями был медленнее подходов, -основанных на разделении памяти, поэтому он и не стал общеприня -тым, но за последнее время здесь многое бесповоротно изменилось. -Параллельные программы на D используют обмен сообщениями – -парадигму, ориентированную на всестороннюю инфраструктурную -поддержку. -- D также поддерживает старомодную синхронизацию на основе кри -тических участков, защищенных мьютексами и флагами событий. -В последнее время этот подход к организации параллельных вычис -лений подвергается серьезной критике за недостаточную масшта -бируемость для настоящих и будущих параллельных архитектур. -D строго управляет разделением данных, ограничивая возможности -программирования с применением блокировок. На первый взгляд -это ограничение может показаться суровым, но оно избавляет осно -ванный на блокировках код от его злейшего врага – низкоуровневых -гонок за данными (ситуаций состязания). При этом разделение дан -ных остается наиболее эффективным средством передачи больших -объемов данных между потоками, так что пренебрегать им не стоит. -- По традиции языков системного уровня программы на D, не имею -щие атрибута `@safe`, могут посредством приведений достигать бес -препятственного разделения данных. За корректность таких про -грамм в основном отвечаете вы. -- Если вам мало предыдущего уровня, конструкция `asm` позволяет по -лучить полный контроль над машинными ресурсами. Для еще бо -лее низкоуровневого контроля потребуются микропаяльник и очень, -очень верная рука. +- Передовой подход к параллельным вычислениям заключается в использовании изолированных потоков или процессов, взаимодействующих с помощью сообщений. Эта парадигма, называемая *обменом сообщениями* (*message passing*), позволяет создавать безопасные модульные программы, легкие для понимания и сопровождения. Обмен сообщениями успешно применяется в разнообразных языках и библиотеках. Раньше обмен сообщениями был медленнее подходов, основанных на разделении памяти, поэтому он и не стал общепринятым, но за последнее время здесь многое бесповоротно изменилось. Параллельные программы на D используют обмен сообщениями – парадигму, ориентированную на всестороннюю инфраструктурную поддержку. +- D также поддерживает старомодную синхронизацию на основе критических участков, защищенных мьютексами и флагами событий. В последнее время этот подход к организации параллельных вычислений подвергается серьезной критике за недостаточную масштабируемость для настоящих и будущих параллельных архитектур. D строго управляет разделением данных, ограничивая возможности программирования с применением блокировок. На первый взгляд это ограничение может показаться суровым, но оно избавляет основанный на блокировках код от его злейшего врага – низкоуровневых гонок за данными (ситуаций состязания). При этом разделение данных остается наиболее эффективным средством передачи больших объемов данных между потоками, так что пренебрегать им не стоит. +- По традиции языков системного уровня программы на D, не имеющие атрибута `@safe`, могут посредством приведений достигать беспрепятственного разделения данных. За корректность таких программ в основном отвечаете вы. +- Если вам мало предыдущего уровня, конструкция `asm` позволяет получить полный контроль над машинными ресурсами. Для еще более низкоуровневого контроля потребуются микропаяльник и очень, очень верная рука. -Прежде чем с головой окунуться во все это, отвлечемся ненадолго, что -бы поближе присмотреться к тем аппаратным усовершенствованиям, -которые потрясли мир. +Прежде чем с головой окунуться во все это, отвлечемся ненадолго, чтобы поближе присмотреться к тем аппаратным усовершенствованиям, которые потрясли мир. [В начало ⮍](#13-параллельные-вычисления) ## 13.1. Революция в области параллельных вычислений -Что касается параллельных вычислений, то для них сейчас времена по -интереснее, чем когда-либо. Это времена, когда и хорошие, и плохие но -вости вписываются в общую панораму компромиссов, противоборств -и тенденций. +Что касается параллельных вычислений, то для них сейчас времена поинтереснее, чем когда-либо. Это времена, когда и хорошие, и плохие новости вписываются в общую панораму компромиссов, противоборств и тенденций. -Хорошие новости в том, что степень интеграции все еще растет по зако -ну Мура[^1]; судя по тому, что нам уже известно, и по тому, что мы сегодня -можем предположить, это продлится как минимум лет десять после -выхода этой книги. Курс на миниатюризацию означает рост плотности -вычислительной мощности пропорционально числу совместно работаю -щих транзисторов на единицу площади. Все ближе друг к другу компо -ненты, все короче соединения, а это означает повышение скорости ло -кальной связности – золотое дно в плане быстродействия. +Хорошие новости в том, что степень интеграции все еще растет по закону Мура[^1]; судя по тому, что нам уже известно, и по тому, что мы сегодня можем предположить, это продлится как минимум лет десять после выхода этой книги. Курс на миниатюризацию означает рост плотности вычислительной мощности пропорционально числу совместно работающих транзисторов на единицу площади. Все ближе друг к другу компоненты, все короче соединения, а это означает повышение скорости локальной связности – золотое дно в плане быстродействия. -К сожалению, отдельные выводы, начинающиеся со слов «к сожале -нию», умеряют энтузиазм по поводу возросшей вычислительной плот -ности. Во-первых, существует не только локальная связность – она фор -мируется в иерархию: тесно связанные компоненты образуют бло -ки, которые должны связываться с другими блоками, образуя блоки -большего размера. В свою очередь, блоки большего размера также со -единяются с другими блоками большего размера, образуя функцио -нальные блоки еще большего размера, и т. д. На своем уровне связности -такие блоки остаются «далеки» друг от друга. Хуже того, возросшая -сложность каждого блока увеличивает сложность связей между блока -ми, что реализуется путем уменьшения толщины проводов и расстоя -ния между ними. Это означает рост сопротивления, электроемкости -и перекрестных помех. Перекрестные помехи – это способность сигнала -из одного провода распространяться на соседние провода посредством -(в данном случае) электромагнитного поля. На высоких частотах про -вод – практически антенна, и помехи становятся настолько невыноси -мыми, что сегодня параллельные соединения все чаще заменяют после -довательными (своего рода феномен нелогичности, заметный на всех -уровнях: USB заменил параллельный порт, в качестве интерфейса на -копителей данных SATA заменил PATA, а в подсистемах памяти после -довательные шины заменяют параллельные, и все из-за перекрестных -помех. Где те золотые деньки, когда параллельное было быстрее, а по -следовательное медленнее?). +К сожалению, отдельные выводы, начинающиеся со слов «к сожалению», умеряют энтузиазм по поводу возросшей вычислительной плотности. Во-первых, существует не только локальная связность – она формируется в иерархию: тесно связанные компоненты образуют блоки, которые должны связываться с другими блоками, образуя блоки большего размера. В свою очередь, блоки большего размера также соединяются с другими блоками большего размера, образуя функциональные блоки еще большего размера, и т. д. На своем уровне связности такие блоки остаются «далеки» друг от друга. Хуже того, возросшая сложность каждого блока увеличивает сложность связей между блоками, что реализуется путем уменьшения толщины проводов и расстояния между ними. Это означает рост сопротивления, электроемкости и перекрестных помех. Перекрестные помехи – это способность сигнала из одного провода распространяться на соседние провода посредством (в данном случае) электромагнитного поля. На высоких частотах провод – практически антенна, и помехи становятся настолько невыносимыми, что сегодня параллельные соединения все чаще заменяют последовательными (своего рода феномен нелогичности, заметный на всех уровнях: USB заменил параллельный порт, в качестве интерфейса накопителей данных SATA заменил PATA, а в подсистемах памяти последовательные шины заменяют параллельные, и все из-за перекрестных помех. Где те золотые деньки, когда параллельное было быстрее, а последовательное медленнее?). -Кроме того, растет разрыв в производительности между вычислитель -ными элементами и памятью. В то время как плотность памяти, как -и ожидалось, увеличивается в соответствии с общей степенью интегра -ции, скорость доступа к ней все больше отстает от скорости вычислений -из-за множества разнообразных физических, технологических и ры -ночных факторов. В настоящее время неясно, что поможет сущест -венно сократить этот разрыв в быстродействии, и он лишь растет. Тыся -чи тактов могут отделять процессор от слова в памяти; а ведь еще не -сколько лет назад можно было купить микросхемы памяти «с нулевым -временем ожидания», обращение к которым осуществлялось за один -такт. +Кроме того, растет разрыв в производительности между вычислительными элементами и памятью. В то время как плотность памяти, как и ожидалось, увеличивается в соответствии с общей степенью интеграции, скорость доступа к ней все больше отстает от скорости вычислений из-за множества разнообразных физических, технологических и рыночных факторов. В настоящее время неясно, что поможет существенно сократить этот разрыв в быстродействии, и он лишь растет. Тысячи тактов могут отделять процессор от слова в памяти; а ведь еще несколько лет назад можно было купить микросхемы памяти «с нулевым временем ожидания», обращение к которым осуществлялось за один такт. -Из-за широкого спектра архитектур памяти, представляющих собой -различные компромиссные решения относительно плотности, цены -и скорости, повысилась и изощренность иерархий памяти; обращение -к единственному слову памяти превратилось в детективное расследова -ние с опросом нескольких уровней кэша, начиная с драгоценного стати -ческого ОЗУ прямо на микросхеме и порой проходя весь путь до массо -вой памяти. Возможна и противоположная ситуация: копии указан -ных данных могут располагаться во множестве мест по всей иерархии. -Это, в свою очередь, тоже влияет на модели программирования. Мы -больше не можем позволить себе представлять память большим моно -литом, удобным для разделения всеми процессами системы: наличие -кэшей провоцирует рост локального трафика в памяти, превращая раз -деляемые данные в иллюзию, которую все труднее сопровождать. +Из-за широкого спектра архитектур памяти, представляющих собой различные компромиссные решения относительно плотности, цены и скорости, повысилась и изощренность иерархий памяти; обращение к единственному слову памяти превратилось в детективное расследование с опросом нескольких уровней кэша, начиная с драгоценного статического ОЗУ прямо на микросхеме и порой проходя весь путь до массовой памяти. Возможна и противоположная ситуация: копии указанных данных могут располагаться во множестве мест по всей иерархии. Это, в свою очередь, тоже влияет на модели программирования. Мы больше не можем позволить себе представлять память большим монолитом, удобным для разделения всеми процессами системы: наличие кэшей провоцирует рост локального трафика в памяти, превращая разделяемые данные в иллюзию, которую все труднее сопровождать. -К последним сенсационным известиям относится то, что скорость света -упрямо решила оставаться неизменной (`immutable`, если хотите) – около -300 000 000 метров в секунду. Скорость же света в оксиде кремния (соот -ветствующая скорости распространения сигнала внутри современных -микросхем) составляет примерно половину этого значения, причем дос -тижимая сегодня скорость переноса самих данных существенно ниже -этого теоретического предела. Это означает больше проблем с глобаль -ной взаимосвязанностью на высоких частотах. Если бы у нас была мик -росхема с частотой 10 ГГц, то простое перемещение бита с одного на -другой конец этого чипа шириной 4,5 см (по сути, вообще без вычисле -ний) в идеальных условиях занимало бы три такта. +К последним сенсационным известиям относится то, что скорость света упрямо решила оставаться неизменной (`immutable`, если хотите) – около 300 000 000 метров в секунду. Скорость же света в оксиде кремния (соответствующая скорости распространения сигнала внутри современных микросхем) составляет примерно половину этого значения, причем достижимая сегодня скорость переноса самих данных существенно ниже этого теоретического предела. Это означает больше проблем с глобальной взаимосвязанностью на высоких частотах. Если бы у нас была микросхема с частотой 10 ГГц, то простое перемещение бита с одного на другой конец этого чипа шириной 4,5 см (по сути, вообще без вычислений) в идеальных условиях занимало бы три такта. -Словом, наступает век процессоров очень высокой плотности и гигант -ской вычислительной мощности, при этом все более изолированных -и труднодоступных, которые сложно использовать из-за ограничений -взаимосвязности, скорости распространения сигнала и быстроты до -ступа к памяти. +Словом, наступает век процессоров очень высокой плотности и гигантской вычислительной мощности, при этом все более изолированных и труднодоступных, которые сложно использовать из-за ограничений взаимосвязности, скорости распространения сигнала и быстроты доступа к памяти. -Компьютерная индустрия, естественно, обходит эти преграды. Одним -из феноменов стало резкое сокращение размеров и энергии, требуемых -для заданной вычислительной мощности; всего лишь пять лет назад -уровень технологии не позволял достичь компактности и возможно -стей КПК, без которых сегодня мы как без рук. При этом традицион -ные компьютеры, пытающиеся повысить вычислительную мощность -при тех же размерах, представляют все меньший интерес. Производи -тели микросхем для них уже не борются за повышение тактовой часто -ты, предлагая взамен вычислительную мощность в уже знакомой упа -ковке: несколько одинаковых центральных процессоров, соединенных -шинами друг с другом и с памятью. Так что спустя каких-то несколько -лет отвечать за разгон компьютеров будут не электронщики, а в основ -ном программисты. Вариант «побольше процессоров» может показать -ся довольно заманчивым, но типовым задачам настольного компьютера -не под силу эффективно использовать и восемь процессоров. В будущем -предполагается экспоненциальный рост числа доступных процессоров -до десятков, сотен и тысяч. При разгоне единственной программы про -граммистам придется очень много потрудиться, чтобы продуктивно ис -пользовать *все* эти процессоры. +Компьютерная индустрия, естественно, обходит эти преграды. Одним из феноменов стало резкое сокращение размеров и энергии, требуемых для заданной вычислительной мощности; всего лишь пять лет назад уровень технологии не позволял достичь компактности и возможностей КПК, без которых сегодня мы как без рук. При этом традиционные компьютеры, пытающиеся повысить вычислительную мощность при тех же размерах, представляют все меньший интерес. Производители микросхем для них уже не борются за повышение тактовой частоты, предлагая взамен вычислительную мощность в уже знакомой упаковке: несколько одинаковых центральных процессоров, соединенных шинами друг с другом и с памятью. Так что спустя каких-то несколько лет отвечать за разгон компьютеров будут не электронщики, а в основном программисты. Вариант «побольше процессоров» может показаться довольно заманчивым, но типовым задачам настольного компьютера не под силу эффективно использовать и восемь процессоров. В будущем предполагается экспоненциальный рост числа доступных процессоров до десятков, сотен и тысяч. При разгоне единственной программы программистам придется очень много потрудиться, чтобы продуктивно использовать *все* эти процессоры. -Из-за разных технологических и человеческих факторов в компьютер -ной индустрии постоянно случаются подвижки и сотрясения, но на -этот раз мы, кажется, дошли до точки. С недавних пор взять отпуск, -чтобы увеличить скорость работы программы, – уже не вариант. Это -возмутительно. Это подрыв устоев. Это революция в области парал -лельных вычислений. +Из-за разных технологических и человеческих факторов в компьютерной индустрии постоянно случаются подвижки и сотрясения, но на этот раз мы, кажется, дошли до точки. С недавних пор взять отпуск, чтобы увеличить скорость работы программы, – уже не вариант. Это возмутительно. Это подрыв устоев. Это революция в области параллельных вычислений. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-1-революция-в-области-параллельных-вычислений) [Наверх ⮍](#13-параллельные-вычисления) ## 13.2. Краткая история механизмов разделения данных -Один из аспектов перемен в компьютерной индустрии – внезапность, -с какой сегодня меняются модели обработки данных и параллелизма, -особенно на фоне темпа развития языков и парадигм программирова -ния. Чтобы язык и связанные с ним стили отпечатались в сознании со -общества программистов, нужны годы, даже десятки лет, а в области -параллелизма начиная с 2000-х все меняется в геометрической про -грессии. +Один из аспектов перемен в компьютерной индустрии – внезапность, с какой сегодня меняются модели обработки данных и параллелизма, особенно на фоне темпа развития языков и парадигм программирования. Чтобы язык и связанные с ним стили отпечатались в сознании сообщества программистов, нужны годы, даже десятки лет, а в области параллелизма начиная с 2000-х все меняется в геометрической прогрессии. -Например, наше прошлогоднее понимание основ параллелизма[^2] тяготе -ло к разделению данных, порожденному мейнфреймами 1960-х. Тогда -процессорное время было настолько дорогим, что повысить общую эф -фективность использования процессора можно было, только разделяя -его между множеством программ, управляемых множеством операто -ров. *Процесс* определялся и определяется как совокупность состояния -и ресурсов исполняющейся программы. Процессор (центральное про -цессорное устройство, ЦПУ) реализует разделение времени с помощью -планировщика задач и прерываний таймера. По каждому прерыванию -таймера планировщик решает, какому процессу предоставить ЦПУ на -следующий квант времени, создавая таким образом иллюзию одновре -менного исполнения нескольких процессов, хотя на самом деле все они -используют одно и то же ЦПУ. +Например, наше прошлогоднее понимание основ параллелизма[^2] тяготело к разделению данных, порожденному мейнфреймами 1960-х. Тогда процессорное время было настолько дорогим, что повысить общую эффективность использования процессора можно было, только разделяя его между множеством программ, управляемых множеством операторов. *Процесс* определялся и определяется как совокупность состояния и ресурсов исполняющейся программы. Процессор (центральное процессорное устройство, ЦПУ) реализует разделение времени с помощью планировщика задач и прерываний таймера. По каждому прерыванию таймера планировщик решает, какому процессу предоставить ЦПУ на следующий квант времени, создавая таким образом иллюзию одновременного исполнения нескольких процессов, хотя на самом деле все они используют одно и то же ЦПУ. -Чтобы ошибочные процессы не повредили друг другу и коду операцион -ной системы, была введена *аппаратная защита памяти*. Для надеж -ной изоляции процессов в современных системах защиту памяти соче -тают с *виртуализацией памяти*: каждый процесс считает память ма -шины «своей собственностью», хотя на самом деле все взаимодействие -между процессом и памятью, а также изоляцию процессов друг от дру -га берет на себя уровень-посредник, транслирующий логические адреса -(так видит память процесс) в физические (так обращается к памяти ма -шина). Хорошие новости в том, что процессы, вышедшие из-под конт -роля, могут навредить только себе, но не другим процессам и не ядру -операционной системы. Новости похуже в том, что каждое переключе -ние задач требует потенциально дорогой смены адресных пространств -процессов, не говоря о том, что каждый процесс при переключении на -него «просыпается» с амнезией кэша, поскольку глобальный кэш обыч -но используется всеми процессами. Так и появились *потоки* (*threads*). -Поток – это процесс, не владеющий информацией о том, как трансли -ровать адреса; это чистый контекст исполнения: состояние процессора -плюс стек. Несколько потоков разделяют адресное пространство про -цесса, то есть порождать потоки и переключаться между ними относи -тельно дешево, и они могут с легкостью и без особых затрат разделять -данные друг с другом. Разделение памяти между потоками, запущен -ными на одном ЦПУ, осуществляется настолько прямолинейно, на -сколько это возможно: один поток пишет, другой читает. При использо -вании техники разделения времени порядок записи данных, естествен -но, совпадает с порядком, в котором эти записи будут видны другим -потокам. Поддержку более высокоуровневых инвариантов данных -обеспечивают механизмы блокировки, например критические секции, -защищенные с помощью примитивов синхронизации (таких как сема -форы и мьютексы). В последние годы XX века то, что можно назвать -«классическим» многопоточным программированием (которое харак -теризуется разделяемым адресным пространством, простыми правила -ми видимости изменений и синхронизацией на мьютексах), обросло -массой наблюдений, народных мудростей и анекдотов. Существовали -и другие модели организации параллельных вычислений, но на боль -шинстве машин применялась классическая многопоточность. +Чтобы ошибочные процессы не повредили друг другу и коду операционной системы, была введена *аппаратная защита памяти*. Для надежной изоляции процессов в современных системах защиту памяти сочетают с *виртуализацией памяти*: каждый процесс считает память машины «своей собственностью», хотя на самом деле все взаимодействие между процессом и памятью, а также изоляцию процессов друг от друга берет на себя уровень-посредник, транслирующий логические адреса (так видит память процесс) в физические (так обращается к памяти машина). Хорошие новости в том, что процессы, вышедшие из-под контроля, могут навредить только себе, но не другим процессам и не ядру операционной системы. Новости похуже в том, что каждое переключение задач требует потенциально дорогой смены адресных пространств процессов, не говоря о том, что каждый процесс при переключении на него «просыпается» с амнезией кэша, поскольку глобальный кэш обычно используется всеми процессами. Так и появились *потоки* (*threads*). Поток – это процесс, не владеющий информацией о том, как транслировать адреса; это чистый контекст исполнения: состояние процессора плюс стек. Несколько потоков разделяют адресное пространство процесса, то есть порождать потоки и переключаться между ними относительно дешево, и они могут с легкостью и без особых затрат разделять данные друг с другом. Разделение памяти между потоками, запущенными на одном ЦПУ, осуществляется настолько прямолинейно, насколько это возможно: один поток пишет, другой читает. При использовании техники разделения времени порядок записи данных, естественно, совпадает с порядком, в котором эти записи будут видны другим потокам. Поддержку более высокоуровневых инвариантов данных обеспечивают механизмы блокировки, например критические секции, защищенные с помощью примитивов синхронизации (таких как семафоры и мьютексы). В последние годы XX века то, что можно назвать «классическим» многопоточным программированием (которое характеризуется разделяемым адресным пространством, простыми правилами видимости изменений и синхронизацией на мьютексах), обросло массой наблюдений, народных мудростей и анекдотов. Существовали и другие модели организации параллельных вычислений, но на большинстве машин применялась классическая многопоточность. -Основные императивные языки наших дней (такие как C, C++, Java) -развивались в век классической многопоточности – в старые добрые -времена простых архитектур памяти, понятных примитивов взаимо -блокировки и разделения данных без изысков. Языки, естественно, мо -делировали реалии аппаратного обеспечения того времени (когда подра -зумевалось, что потоки разделяют одну и ту же область памяти) и вклю -чали соответствующие средства. В конце концов само определение мно -гопоточности подразумевает, что все потоки, в отличие от процессов -операционной системы, разделяют одно общее адресное пространство. -Кроме того, API для реализации обмена сообщениями (например, спе -цификация MPI [29]) были доступны лишь в форме библиотек, изна -чально созданных для специализированного дорогостоящего аппарат -ного обеспечения, такого как кластеры (супер)компьютеров. +Основные императивные языки наших дней (такие как C, C++, Java) развивались в век классической многопоточности – в старые добрые времена простых архитектур памяти, понятных примитивов взаимоблокировки и разделения данных без изысков. Языки, естественно, моделировали реалии аппаратного обеспечения того времени (когда подразумевалось, что потоки разделяют одну и ту же область памяти) и включали соответствующие средства. В конце концов само определение многопоточности подразумевает, что все потоки, в отличие от процессов операционной системы, разделяют одно общее адресное пространство. Кроме того, API для реализации обмена сообщениями (например, спецификация MPI [29]) были доступны лишь в форме библиотек, изначально созданных для специализированного дорогостоящего аппаратного обеспечения, такого как кластеры (супер)компьютеров. -Тогда еще только зарождающиеся функциональные языки заняли прин -ципиальную позицию, основанную на математической чистоте: «Мы не -заинтересованы в моделировании аппаратного обеспечения, – сказали -они. – Нам хотелось бы моделировать математику». А в математике ред -ко что-то меняется, математические результаты инвариантны во време -ни, что делает математические вычисления идеальным кандидатом для -распараллеливания. (Только представьте, как первые программисты – -вчерашние математики, услышав о параллельных вычислениях, чешут -затылки, восклицая: «Минуточку!..») Функциональные программисты -убеждены, что такая модель вычислений поощряет неупорядоченное, -параллельное выполнение, однако до недавнего времени эта возможно -сть являлась скорее потенциальной энергией, чем достигнутой целью. -Наконец был разработан язык Erlang. Он начал свой путь в конце 1980-х -как предметно-ориентированный встроенный язык приложений для -телефонии. Предметная область, предполагая десятки тысяч программ, -одновременно запущенных на одной машине, заставляла отдать пред -почтение обмену сообщениями, когда информация передается в стиле -«выстрелил – забыл». Аппаратное обеспечение и операционные систе -мы по большей части не были оптимизированы для таких нагрузок, но -Erlang изначально запускался на специализированной платформе. В ре -зультате получился язык, оригинальным образом сочетающий нечис -тый функциональный стиль, серьезные возможности для параллель -ных вычислений и стойкое предпочтение обмена сообщениями (ника -кого разделения памяти!). +Тогда еще только зарождающиеся функциональные языки заняли принципиальную позицию, основанную на математической чистоте: «Мы не заинтересованы в моделировании аппаратного обеспечения, – сказали они. – Нам хотелось бы моделировать математику». А в математике редко что-то меняется, математические результаты инвариантны во времени, что делает математические вычисления идеальным кандидатом для распараллеливания. (Только представьте, как первые программисты – вчерашние математики, услышав о параллельных вычислениях, чешут затылки, восклицая: «Минуточку!..») Функциональные программисты убеждены, что такая модель вычислений поощряет неупорядоченное, параллельное выполнение, однако до недавнего времени эта возможность являлась скорее потенциальной энергией, чем достигнутой целью. Наконец был разработан язык Erlang. Он начал свой путь в конце 1980-х как предметно-ориентированный встроенный язык приложений для телефонии. Предметная область, предполагая десятки тысяч программ, одновременно запущенных на одной машине, заставляла отдать предпочтение обмену сообщениями, когда информация передается в стиле «выстрелил – забыл». Аппаратное обеспечение и операционные системы по большей части не были оптимизированы для таких нагрузок, но Erlang изначально запускался на специализированной платформе. В результате получился язык, оригинальным образом сочетающий нечистый функциональный стиль, серьезные возможности для параллельных вычислений и стойкое предпочтение обмена сообщениями (никакого разделения памяти!). -Перенесемся в 2010-е. Сегодня даже у средних машин больше одного -процессора, а главная задача десятилетия – уместить на кристалле как -можно больше ЦПУ. Отсюда и последствия, самое важное из которых – -конец монолитной разделяемой памяти. +Перенесемся в 2010-е. Сегодня даже у средних машин больше одного процессора, а главная задача десятилетия – уместить на кристалле как можно больше ЦПУ. Отсюда и последствия, самое важное из которых – конец монолитной разделяемой памяти. -С одним разделяемым по времени ЦПУ связана одна подсистема памя -ти – с буферами, несколькими уровнями кэшей, все по полной програм -ме. Независимо от того, как ЦПУ управляет разделением времени, чте -ние и запись проходят по одному и тому же маршруту, а потому видение -памяти у разных потоков остается когерентным. Несколько взаимосвя -занных ЦПУ, напротив, не могут позволить себе разделять подсистему -кэша: такой кэш потребовал бы мультипортового доступа (что дорого -и слабо масштабируемо), и его было бы трудно разместить в непосредст -венной близости ко всем ЦПУ сразу. Вот почему практически все совре -менные ЦПУ производятся со своей кэш-памятью, предназначенной -лишь для их собственных нужд. Производительность мультипроцес -сорной системы зависит главным образом от аппаратного обеспечения -и протоколов, соединяющих комплексы ЦПУ+кэш. +С одним разделяемым по времени ЦПУ связана одна подсистема памяти – с буферами, несколькими уровнями кэшей, все по полной программе. Независимо от того, как ЦПУ управляет разделением времени, чтение и запись проходят по одному и тому же маршруту, а потому видение памяти у разных потоков остается когерентным. Несколько взаимосвязанных ЦПУ, напротив, не могут позволить себе разделять подсистему кэша: такой кэш потребовал бы мультипортового доступа (что дорого и слабо масштабируемо), и его было бы трудно разместить в непосредственной близости ко всем ЦПУ сразу. Вот почему практически все современные ЦПУ производятся со своей кэш-памятью, предназначенной лишь для их собственных нужд. Производительность мультипроцессорной системы зависит главным образом от аппаратного обеспечения и протоколов, соединяющих комплексы ЦПУ+кэш. -Несколько кэшей превращают разделение данных между потоками -в чертовски сложную задачу. Теперь операции чтения и записи в раз -ных потоках могут затрагивать разные кэши, поэтому сделать так, что -бы один поток делился данными с другим, стало сложнее, чем раньше. -На самом деле, этот процесс превращается в своего рода обмен сообще -ниями[^3]: в каждом случае такого разделения между подсистемами кэ -шей должно иметь место что-то вроде рукопожатия, обеспечивающего -попадание разделяемых данных от последнего записавшего потока к чи -тающему потоку, а также в основную память. +Несколько кэшей превращают разделение данных между потоками в чертовски сложную задачу. Теперь операции чтения и записи в разных потоках могут затрагивать разные кэши, поэтому сделать так, чтобы один поток делился данными с другим, стало сложнее, чем раньше. На самом деле, этот процесс превращается в своего рода обмен сообщениями[^3]: в каждом случае такого разделения между подсистемами кэшей должно иметь место что-то вроде рукопожатия, обеспечивающего попадание разделяемых данных от последнего записавшего потока к читающему потоку, а также в основную память. -Протоколы синхронизации кэшей добавляют к сюжету еще один пово -рот (хотя и без него все было достаточно лихо закручено): они восприни -мают данные только блоками, не предусматривая чтение и запись от -дельных слов. То есть общающиеся друг с другом процессы «не помнят» -точный порядок, в котором записывались данные, что приводит к пара -доксальному поведению, которое не поддается разумному объяснению -и противоречит здравому смыслу: один поток записывает x, а затем y, -и в некоторый промежуток времени другой поток видит новое y, но ста -рое x. Такие нарушения причинно-следственных связей слабо вписыва -ются в общую модель классической многопоточности. Даже наиболее -сведущим в классической многопоточности программистам невероятно -трудно адаптировать свой стиль и шаблоны программирования к но -вым архитектурам памяти. +Протоколы синхронизации кэшей добавляют к сюжету еще один поворот (хотя и без него все было достаточно лихо закручено): они воспринимают данные только блоками, не предусматривая чтение и запись отдельных слов. То есть общающиеся друг с другом процессы «не помнят» точный порядок, в котором записывались данные, что приводит к парадоксальному поведению, которое не поддается разумному объяснению и противоречит здравому смыслу: один поток записывает `x`, а затем `y`, и в некоторый промежуток времени другой поток видит новое `y`, но старое `x`. Такие нарушения причинно-следственных связей слабо вписываются в общую модель классической многопоточности. Даже наиболее сведущим в классической многопоточности программистам невероятно трудно адаптировать свой стиль и шаблоны программирования к новым архитектурам памяти. -Проиллюстрируем скоростные изменения в современных параллель -ных вычислениях и серьезное влияние разделения данных на подходы -языков к параллелизму советом из чудесной книги «Java. Эффективное -программирование» издания 2001 года [8, разд. 51, с. 204]: +Проиллюстрируем скоростные изменения в современных параллельных вычислениях и серьезное влияние разделения данных на подходы языков к параллелизму советом из чудесной книги «Java. Эффективное программирование» издания 2001 года [8, разд. 51, с. 204]: -> «Если есть несколько готовых к исполнению потоков, планировщик пото -ков определит, какие потоки должны запуститься и на какое время… Луч -ший способ написать отказоустойчивое, оперативное и переносимое прило -жение – стараться иметь минимум готовых к исполнению потоков в любой -момент времени.» +> «Если есть несколько готовых к исполнению потоков, планировщик потоков определит, какие потоки должны запуститься и на какое время… Лучший способ написать отказоустойчивое, оперативное и переносимое приложение – стараться иметь минимум готовых к исполнению потоков в любой момент времени.» -Сегодняшний читатель сразу же отметит поразительную деталь: здесь -не просто говорится об однопроцессорном программировании с много -поточностью на основе разделения времени, но подразумевается един -ственность процессора, хоть и без явной констатации. Естественно, что -в издании 2008 года[^4] этот совет был изменен на «стремиться к тому, -чтобы среднее число готовых к исполнению потоков было ненамного -больше числа процессоров». Любопытно, что даже этот совет, на вид ра -зумный, подразумевает два невысказанных допущения: 1) за счет дан -ных потоки будут сильно связаны друг с другом, что в свою очередь -приведет к снижению быстродействия из-за накладных расходов на -взаимоблокировки, и 2) число процессоров на машинах, где может за -пускаться программа, примерно одинаково. И тогда этот совет полно -стью противоположен тому, что настойчиво повторяется в книге «Про -граммирование на языке Erlang» [5, глава 20, с. 363]: +Сегодняшний читатель сразу же отметит поразительную деталь: здесь не просто говорится об однопроцессорном программировании с многопоточностью на основе разделения времени, но подразумевается единственность процессора, хоть и без явной констатации. Естественно, что в издании 2008 года[^4] этот совет был изменен на «стремиться к тому, чтобы среднее число готовых к исполнению потоков было ненамного больше числа процессоров». Любопытно, что даже этот совет, на вид разумный, подразумевает два невысказанных допущения: 1) за счет данных потоки будут сильно связаны друг с другом, что в свою очередь приведет к снижению быстродействия из-за накладных расходов на взаимоблокировки, и 2) число процессоров на машинах, где может запускаться программа, примерно одинаково. И тогда этот совет полностью противоположен тому, что настойчиво повторяется в книге «Программирование на языке Erlang» [5, глава 20, с. 363]: -> «**Используйте много процессоров**. Это важно: мы должны держать свои -ЦПУ в занятом состоянии. Все ЦПУ должны быть заняты в каждый момент -времени. Легче всего достигнуть этого, имея много процессов[^5]. Говоря „мно -го процессов“, я имею в виду много по отношению к количеству ЦПУ. Если -у нас много процессов, то о занятом состоянии для ЦПУ можно не беспоко -иться.» +> «**Используйте много процессоров**. Это важно: мы должны держать свои ЦПУ в занятом состоянии. Все ЦПУ должны быть заняты в каждый момент времени. Легче всего достигнуть этого, имея много процессов[^5]. Говоря „много процессов“, я имею в виду много по отношению к количеству ЦПУ. Если у нас много процессов, то о занятом состоянии для ЦПУ можно не беспокоиться.» -Какой из этих трех рекомендаций следовать? Как обычно, все зависит от -обстоятельств. Первая прекрасно подходит для аппаратного обеспечения -2001 года; вторая – для сценариев, характеризующихся интенсивной -работой с разделяемыми данными и, следовательно, жестким соперни -чеством; третья полезна в условиях слабого соперничества и большого -количества ЦПУ. +Какой из этих трех рекомендаций следовать? Как обычно, все зависит от обстоятельств. Первая прекрасно подходит для аппаратного обеспечения 2001 года; вторая – для сценариев, характеризующихся интенсивной работой с разделяемыми данными и, следовательно, жестким соперничеством; третья полезна в условиях слабого соперничества и большого количества ЦПУ. -Поддерживать разделение памяти все сложнее, этот подход к организа -ции параллельных вычислений начинает казаться неубедительным, -в моду входят функциональность и обмен сообщениями. Неудивитель -но, что в последние годы растет интерес к Erlang и другим функцио -нальным языкам, удобным для разработки приложений с параллель -ными вычислениями. +Поддерживать разделение памяти все сложнее, этот подход к организации параллельных вычислений начинает казаться неубедительным, в моду входят функциональность и обмен сообщениями. Неудивительно, что в последние годы растет интерес к Erlang и другим функциональным языкам, удобным для разработки приложений с параллельными вычислениями. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-2-краткая-история-механизмов-разделения-данных) [Наверх ⮍](#13-параллельные-вычисления) ## 13.3. Смотри, мам, никакого разделения (по умолчанию) -Вследствие последних усовершенствований аппаратного и программ -ного обеспечения D решил отойти от других императивных языков: да, -язык D поддерживает потоки, но они не разделяют никакие изменяе -мые данные по умолчанию – они изолированы друг от друга. Изоляция -обеспечивается не аппаратно (как в случае с процессами) и не с помо -щью проверок времени исполнения; она является естественным следст -вием устройства системы типов D. +Вследствие последних усовершенствований аппаратного и программного обеспечения D решил отойти от других императивных языков: да, язык D поддерживает потоки, но они не разделяют никакие изменяемые данные по умолчанию – они изолированы друг от друга. Изоляция обеспечивается не аппаратно (как в случае с процессами) и не с помощью проверок времени исполнения; она является естественным следствием устройства системы типов D. -Это решение в духе функциональных языков, которые также старают -ся запретить любые изменения, а значит, и разделение изменяемых -данных. Но есть два различия. Во-первых, программы на D все же мо -гут свободно использовать изменяемые данные – закрыта лишь воз -можность непреднамеренного обращения к изменяемым данным для -других потоков. Во-вторых, «никакого разделения» – это лишь выбор -*по умолчанию*, но не *единственный* возможный. Чтобы определить дан -ные как разделяемые между потоками, необходимо уточнить их опре -деление с помощью ключевого слова `shared`. Рассмотрим пример двух -простых определений, размещенных в корне модуля: +Это решение в духе функциональных языков, которые также стараются запретить любые изменения, а значит, и разделение изменяемых данных. Но есть два различия. Во-первых, программы на D все же могут свободно использовать изменяемые данные – закрыта лишь возможность непреднамеренного обращения к изменяемым данным для других потоков. Во-вторых, «никакого разделения» – это лишь выбор *по умолчанию*, но не *единственный* возможный. Чтобы определить данные как разделяемые между потоками, необходимо уточнить их определение с помощью ключевого слова `shared`. Рассмотрим пример двух простых определений, размещенных в корне модуля: ```d int perThread; shared int perProcess; ``` -В большинстве языков первое определение (или его синтаксический эк -вивалент) означало бы ввод глобальной переменной, используемой все -ми потоками, но в D у переменной `perThread` есть отдельная копия для -каждого потока. Второе определение выделяет память лишь под одно -значение типа `int`, разделяемое всеми потоками, так что в некотором ро -де оно ближе (но не идентично) к традиционной глобальной переменной. +В большинстве языков первое определение (или его синтаксический эквивалент) означало бы ввод глобальной переменной, используемой всеми потоками, но в D у переменной `perThread` есть отдельная копия для каждого потока. Второе определение выделяет память лишь под одно значение типа `int`, разделяемое всеми потоками, так что в некотором роде оно ближе (но не идентично) к традиционной глобальной переменной. -Переменная `perThread` сохраняется при помощи средства операционной -системы, называемого локальным хранилищем потока (thread-local -storage, TLS). Скорость доступа к данным, память под которые выделе -на в TLS, зависит от реализации компилятора и базовой операционной -системы. В общем случае эта скорость лишь незначительно меньше, -скажем, скорости обращения к обычной глобальной переменной в про -грамме на C. В редких случаях, когда эта разница может иметь значе -ние, например в циклах, где делается множество обращений к перемен -ной в TLS, можно загрузить глобальную переменную в стековую. +Переменная `perThread` сохраняется при помощи средства операционной системы, называемого локальным хранилищем потока (thread-local storage, TLS). Скорость доступа к данным, память под которые выделена в TLS, зависит от реализации компилятора и базовой операционной системы. В общем случае эта скорость лишь незначительно меньше, скажем, скорости обращения к обычной глобальной переменной в программе на C. В редких случаях, когда эта разница может иметь значение, например в циклах, где делается множество обращений к переменной в TLS, можно загрузить глобальную переменную в стековую. -У такого подхода есть два важных преимущества. Во-первых, языки, -по умолчанию использующие разделение, должны тщательно синхро -низировать доступ к глобальным данным; для `perThread` же это необяза -тельно, потому что у каждого потока есть ее локальная копия. Во-вто -рых, квалификатор `shared` означает, что и система типов, и програм -мист в курсе, что к переменной `perProcess` одновременно обращаются -многие потоки. В частности, система типов активно защищает разде -ляемые данные, запрещая использовать их очевидно некорректным об -разом. D переворачивает традиционные представления с ног на голову: -в режиме разделения по умолчанию программист обязан вручную от -слеживать, какие данные разделяются, а какие нет, и ведь на самом де -ле, большинство ошибок, имеющих место при параллельных вычисле -ниях, бывают вызваны чрезмерным или незащищенным разделением -данных. В режиме явного разделения программист точно знает, что -данные, не помеченные квалификатором `shared`, действительно будут -видны только одному потоку. (Для обеспечения такой гарантии значе -ния с пометкой `shared` проходят дополнительные проверки, до которых -мы скоро доберемся.) +У такого подхода есть два важных преимущества. Во-первых, языки, по умолчанию использующие разделение, должны тщательно синхронизировать доступ к глобальным данным; для `perThread` же это необязательно, потому что у каждого потока есть ее локальная копия. Во-вторых, квалификатор `shared` означает, что и система типов, и программист в курсе, что к переменной `perProcess` одновременно обращаются многие потоки. В частности, система типов активно защищает разделяемые данные, запрещая использовать их очевидно некорректным образом. D переворачивает традиционные представления с ног на голову: в режиме разделения по умолчанию программист обязан вручную отслеживать, какие данные разделяются, а какие нет, и ведь на самом деле, большинство ошибок, имеющих место при параллельных вычислениях, бывают вызваны чрезмерным или незащищенным разделением данных. В режиме явного разделения программист точно знает, что данные, не помеченные квалификатором `shared`, действительно будут видны только одному потоку. (Для обеспечения такой гарантии значения с пометкой `shared` проходят дополнительные проверки, до которых мы скоро доберемся.) -Использование разделяемых данных остается делом не для новичков, -поскольку, хотя система типов и обеспечивает низкоуровневую коге -рентность, автоматически обеспечить соблюдение высокоуровневых -инвариантов невозможно. Наиболее предпочтительный метод органи -зации безопасного, простого и эффективного обмена информацией меж -ду потоками – использовать парадигму *обмена сообщениями*. Обладаю -щие изолированной памятью потоки взаимодействуют, отправляя друг -другу асинхронные сообщения, состоящие попросту из совместно упа -кованных значений D. +Использование разделяемых данных остается делом не для новичков, поскольку, хотя система типов и обеспечивает низкоуровневую когерентность, автоматически обеспечить соблюдение высокоуровневых инвариантов невозможно. Наиболее предпочтительный метод организации безопасного, простого и эффективного обмена информацией между потоками – использовать парадигму *обмена сообщениями*. Обладающие изолированной памятью потоки взаимодействуют, отправляя друг другу асинхронные сообщения, состоящие попросту из совместно упакованных значений D. -Изолированные работники, общающиеся друг с другом с помощью про -стых каналов коммуникации, – это очень надежный, проверенный вре -менем подход к параллелизму. Язык Erlang и приложения, использую -щие спецификацию интерфейса передачи сообщений (Message Passing -Interface, MPI), применяют его уже давно. +Изолированные работники, общающиеся друг с другом с помощью простых каналов коммуникации, – это очень надежный, проверенный временем подход к параллелизму. Язык Erlang и приложения, использующие спецификацию интерфейса передачи сообщений (Message Passing Interface, MPI), применяют его уже давно. -> Намажем мед на пластырь[^6]. Даже в языках, использующих разделение дан -ных по умолчанию, хорошая практика программирования фактически -предписывает изолировать потоки. Герб Саттер, известный эксперт по па -раллельным вычислениям, в статье с красноречивым названием «Исполь -зуйте потоки правильно = изоляция + асинхронные сообщения» пишет: -«Потоки – это низкоуровневый инструмент для выражения асинхронных -действий. „Приподнимите“ их, установив строгую дисциплину: старайтесь -делать их данные локальными, а синхронизацию и обмен информацией ор -ганизовывать через асинхронные сообщения. Всякий поток, которому нуж -но получать информацию от других потоков или от людей, должен иметь -очередь сообщений (простую очередь FIFO или очередь с приоритетами) и ор -ганизовывать свою работу, ориентируясь на управляемую событиями пом -повую магистраль сообщений; замена запутанной логики событийной логи -кой – чудесный способ улучшить ясность и детерминированность кода.» +> Намажем мед на пластырь[^6]. Даже в языках, использующих разделение данных по умолчанию, хорошая практика программирования фактически предписывает изолировать потоки. Герб Саттер, известный эксперт по параллельным вычислениям, в статье с красноречивым названием «Используйте потоки правильно = изоляция + асинхронные сообщения» пишет: «Потоки – это низкоуровневый инструмент для выражения асинхронных действий. „Приподнимите“ их, установив строгую дисциплину: старайтесь делать их данные локальными, а синхронизацию и обмен информацией организовывать через асинхронные сообщения. Всякий поток, которому нужно получать информацию от других потоков или от людей, должен иметь очередь сообщений (простую очередь FIFO или очередь с приоритетами) и организовывать свою работу, ориентируясь на управляемую событиями помповую магистраль сообщений; замена запутанной логики событийной логикой – чудесный способ улучшить ясность и детерминированность кода.» -Если и есть что-то, чему нас научили десятилетия компьютерных вы -числений, так это то, что программирование на базе дисциплины не -масштабируется[^7]. Но пользователи D могут вздохнуть с облегчением: -в данной цитате в основном очень точно изложены тезисы нескольких -следующих частей – кроме того, что касается дисциплины. +Если и есть что-то, чему нас научили десятилетия компьютерных вычислений, так это то, что программирование на базе дисциплины не масштабируется[^7]. Но пользователи D могут вздохнуть с облегчением: в данной цитате в основном очень точно изложены тезисы нескольких следующих частей – кроме того, что касается дисциплины. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-3-смотри-мам-никакого-разделения-по-умолчанию) [Наверх ⮍](#13-параллельные-вычисления) ## 13.4. Запускаем поток @@ -526,52 +156,19 @@ void fun(int low, int high) } ``` -Функция `spawn` принимает адрес функции `fun` и список аргументов `‹a1›`, -`‹a2›`, ..., `‹an›`. Число аргументов `n` и их типы должны соответствовать сиг -натуре функции `fun`, иными словами, вызов `fun(‹a1›, ‹a2›, ..., ‹an›)` дол -жен быть корректным. Эта проверка выполняется во время компиля -ции. `spawn` создает новый поток выполнения, который инициирует вы -зов `fun(‹a1›, ‹a2›, ..., ‹an›)`, а затем завершает свое выполнение. Конечно -же, функция `spawn` не ждет, когда поток закончит выполняться, – она -возвращает управление сразу же после создания потока и передачи ему -аргументов (в данном случае двух целых чисел). +Функция `spawn` принимает адрес функции `fun` и список аргументов `‹a1›`, `‹a2›`, ..., `‹an›`. Число аргументов `n` и их типы должны соответствовать сигнатуре функции `fun`, иными словами, вызов `fun(‹a1›, ‹a2›, ..., ‹an›)` должен быть корректным. Эта проверка выполняется во время компиляции. `spawn` создает новый поток выполнения, который инициирует вызов `fun(‹a1›, ‹a2›, ..., ‹an›)`, а затем завершает свое выполнение. Конечно же, функция `spawn` не ждет, когда поток закончит выполняться, – она возвращает управление сразу же после создания потока и передачи ему аргументов (в данном случае двух целых чисел). -Эта программа выводит в стандартный поток вывода в общей сложно -сти 200 строк. Порядок следования этих строк зависит от множества -факторов; вполне возможно, что вы увидите 100 строк из основного по -тока, а затем 100 строк из побочного, в точности противоположную по -следовательность или некоторое чередование, кажущееся случайным. -Однако в одной строке никогда не появится смесь из двух сообщений. -Потому что функция `writeln` определена так, чтобы каждый вызов был -атомарен по отношению к потоку вывода. Кроме того, порядок строк, -в котором они порождаются каждым из потоков, также будет соблюден. +Эта программа выводит в стандартный поток вывода в общей сложности 200 строк. Порядок следования этих строк зависит от множества факторов; вполне возможно, что вы увидите 100 строк из основного потока, а затем 100 строк из побочного, в точности противоположную последовательность или некоторое чередование, кажущееся случайным. Однако в одной строке никогда не появится смесь из двух сообщений. Потому что функция `writeln` определена так, чтобы каждый вызов был атомарен по отношению к потоку вывода. Кроме того, порядок строк, в котором они порождаются каждым из потоков, также будет соблюден. -Даже если выполнение `main` завершится до окончания выполнения `fun` -в дочернем потоке, программа будет терпеливо ждать того момента, ко -гда завершатся все потоки, и только тогда завершит свое выполнение. -Ведь библиотека поддержки времени исполнения подчиняется неболь -шому протоколу завершения выполнения программ, о котором мы по -говорим позже; а пока лишь возьмем на заметку, что даже если `main` воз -вращает управление, другие потоки не умирают тут же. +Даже если выполнение `main` завершится до окончания выполнения `fun` в дочернем потоке, программа будет терпеливо ждать того момента, когда завершатся все потоки, и только тогда завершит свое выполнение. Ведь библиотека поддержки времени исполнения подчиняется небольшому протоколу завершения выполнения программ, о котором мы поговорим позже; а пока лишь возьмем на заметку, что даже если `main` возвращает управление, другие потоки не умирают тут же. -Как и было обещано, у только что созданного потока нет ничего общего -с потоком, инициировавшим его. Ну, почти ничего: глобальный де -скриптор файла `stdout` *де факто* разделяется между всеми потоками. -Но все же жульничества тут нет: если вы взглянете на реализацию мо -дуля `std.stdio`, то увидите, что `stdout` определяется там как глобальная -разделяемая переменная. Все грамотно просчитано в системе типов. +Как и было обещано, у только что созданного потока нет ничего общего с потоком, инициировавшим его. Ну, почти ничего: глобальный дескриптор файла `stdout` *де факто* разделяется между всеми потоками. Но все же жульничества тут нет: если вы взглянете на реализацию модуля `std.stdio`, то увидите, что `stdout` определяется там как глобальная разделяемая переменная. Все грамотно просчитано в системе типов. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-4-запускаем-поток) [Наверх ⮍](#13-параллельные-вычисления) ### 13.4.1. Неизменяемое разделение -Какие именно функции можно вызывать из `spawn`? Установка на отсут -ствие разделения налагает определенные ограничения: в функцию, за -пускающую поток (в рассмотренном выше примере это функция `fun`), -параметры можно передавать лишь по значению. Любая передача по -ссылке, как явная (в виде параметра с квалификатором `ref`), так и неяв -ная (например, с помощью массива), должна быть под запретом. Имея -в виду это условие, обратимся к новой версии предыдущего примера: +Какие именно функции можно вызывать из `spawn`? Установка на отсутствие разделения налагает определенные ограничения: в функцию, запускающую поток (в рассмотренном выше примере это функция `fun`), параметры можно передавать лишь по значению. Любая передача по ссылке, как явная (в виде параметра с квалификатором `ref`), так и неявная (например, с помощью массива), должна быть под запретом. Имея в виду это условие, обратимся к новой версии предыдущего примера: ```d import std.concurrency, std.stdio; @@ -596,36 +193,15 @@ void fun(string text, int low, int high) } ``` -Переписанный пример идентичен исходному, за исключением того, что -печатает еще одну строку. Эта строка создается в основном потоке и пе -редается в дочерний поток без копирования. По сути, содержание `message` -разделяется между потоками. Таким образом, нарушен вышеупомяну -тый принцип, гласящий, что любое разделение данных должно быть -явно помечено ключевым словом `shared`. Тем не менее код этого примера -компилируется и запускается. Что же происходит? +Переписанный пример идентичен исходному, за исключением того, что печатает еще одну строку. Эта строка создается в основном потоке и передается в дочерний поток без копирования. По сути, содержание `message` разделяется между потоками. Таким образом, нарушен вышеупомянутый принцип, гласящий, что любое разделение данных должно быть явно помечено ключевым словом `shared`. Тем не менее код этого примера компилируется и запускается. Что же происходит? -В главе 8 сообщается, что квалификатор `immutable` предоставляет серь -езные гарантии: гарантируется, что помеченное этим ключевым словом -значение ни разу не изменится за всю свою жизнь. В той же главе объ -ясняется (см. раздел 8.2), что тип `string` – это на самом деле псевдоним -для типа `immutable(char)[]`. Наконец, мы знаем, что все споры возникают -из-за разделения *изменяемых* данных – пока никто данные не изменя -ет, можно свободно разделять их, ведь все будут видеть в точности одно -и то же. Система типов и инфраструктура потоков в целом признают -этот факт, разрешая разделять между потоками все данные, помечен -ные квалификатором `immutable`. В частности, можно разделять значе -ния типа `string`, отдельные знаки которых изменить невозможно. На -самом деле, своим появлением в языке квалификатор `immutable` не в по -следнюю очередь обязан той помощи, которую он оказывает при разде -лении структурированных данных между потоками. +В главе 8 сообщается, что квалификатор `immutable` предоставляет серьезные гарантии: гарантируется, что помеченное этим ключевым словом значение ни разу не изменится за всю свою жизнь. В той же главе объясняется (см. раздел 8.2), что тип `string` – это на самом деле псевдоним для типа `immutable(char)[]`. Наконец, мы знаем, что все споры возникают из-за разделения *изменяемых* данных – пока никто данные не изменяет, можно свободно разделять их, ведь все будут видеть в точности одно и то же. Система типов и инфраструктура потоков в целом признают этот факт, разрешая разделять между потоками все данные, помеченные квалификатором `immutable`. В частности, можно разделять значения типа `string`, отдельные знаки которых изменить невозможно. На самом деле, своим появлением в языке квалификатор `immutable` не в последнюю очередь обязан той помощи, которую он оказывает при разделении структурированных данных между потоками. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-4-1-неизменяемое-разделение) [Наверх ⮍](#13-параллельные-вычисления) ## 13.5. Обмен сообщениями между потоками -Потоки, печатающие сообщения в произвольном порядке, малоинтерес -ны. Изменим наш пример так, чтобы обеспечить работу потоков в тан -деме. Добьемся, чтобы они печатали сообщения следующим образом: +Потоки, печатающие сообщения в произвольном порядке, малоинтересны. Изменим наш пример так, чтобы обеспечить работу потоков в тандеме. Добьемся, чтобы они печатали сообщения следующим образом: ``` Основной поток: 0 @@ -637,24 +213,9 @@ void fun(string text, int low, int high) Дочерний поток: 99 ``` -Для этого потребуется определить небольшой протокол взаимодейст -вия двух потоков: основной поток должен отправлять дочернему пото -ку сообщение «Напечатай это число», а дочерний – отвечать «Печать -завершена». Вряд ли здесь имеют место какие-либо параллельные вы -числения, но такой пример наглядно объясняет, как организуется взаи -модействие в чистом виде. В настоящих приложениях большую часть -своего времени потоки должны заниматься полезной работой, а на об -щение тратить лишь сравнительно малое время. +Для этого потребуется определить небольшой протокол взаимодействия двух потоков: основной поток должен отправлять дочернему потоку сообщение «Напечатай это число», а дочерний – отвечать «Печать завершена». Вряд ли здесь имеют место какие-либо параллельные вычисления, но такой пример наглядно объясняет, как организуется взаимодействие в чистом виде. В настоящих приложениях большую часть своего времени потоки должны заниматься полезной работой, а на общение тратить лишь сравнительно малое время. -Начнем с того, что для взаимодействия двух потоков им требуется знать, -как обращаться друг к другу. В программе может быть много перегова -ривающихся потоков, так что средство идентификации необходимо. -Чтобы обратиться к потоку, нужно получить возвращаемый функцией -`spawn` *идентификатор потока* (*thread id*), который с этих пор мы будем -неофициально называть «tid». (Тип tid так и называется – `Tid`.) Дочер -нему потоку, в свою очередь, также нужен tid, для того чтобы отпра -вить ответ. Это легко организовать, заставив отправителя указать соб -ственный `Tid`, как пишут адрес отправителя на конверте. Вот этот код: +Начнем с того, что для взаимодействия двух потоков им требуется знать, как обращаться друг к другу. В программе может быть много переговаривающихся потоков, так что средство идентификации необходимо. Чтобы обратиться к потоку, нужно получить возвращаемый функцией `spawn` *идентификатор потока* (*thread id*), который с этих пор мы будем неофициально называть «tid». (Тип tid так и называется – `Tid`.) Дочернему потоку, в свою очередь, также нужен tid, для того чтобы отправить ответ. Это легко организовать, заставив отправителя указать собственный `Tid`, как пишут адрес отправителя на конверте. Вот этот код: ```d import std.concurrency, std.stdio, std.exception; @@ -682,67 +243,23 @@ void writer() } ``` -Теперь функции `writer` аргументы не нужны: всю необходимую инфор -мацию она получает в форме сообщений. Основной поток сохраняет `Tid`, -возвращенный функцией `spawn`, а затем использует его при вызове мето -да `send`. С помощью этого вызова другому потоку отправляются два -фрагмента данных: `Tid` текущего потока (доступ к которому предостав -ляет глобальная переменная `thisTid`) и целое число, которое нужно на -печатать. Перекинув данные через забор другому потоку, основной по -ток начинает ждать подтверждение того, что его сообщение получено, -в виде вызова `receiveOnly`. Функции `send` и `receiveOnly` работают в танде -ме: всякому вызову `send` в одном потоке ставится в соответствие вызов -`receiveOnly` в другом. В названии `receiveOnly` присутствует слово «only» -(только), потому что `receiveOnly` принимает только определенные типы, -например, инициатор вызова `receiveOnly!bool()` принимает лишь сооб -щения в виде логических значений; если другой поток отправляет что- -либо другое, `receiveOnly` порождает исключение типа `MessageMismatch`. +Теперь функции `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`. +Предоставим `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`. +Типом `msg` является `Tuple!(Tid, int)`. В общем случае сообщения со множеством аргументов упаковываются в кортежи, так что одному члену кортежа соответствует один аргумент. Но если сообщение состоит всего из одного значения, лишние движения не нужны, и упаковка в `Tuple` опускается. Например, `receiveOnly!int()` возвращает `int`, а не `Tuple!int`. -Продолжим разбор `writer`. Следующая строка, собственно, выполняет -печать (запись в консоль). Вспомните, что для кортежа `msg` выражение -`msg[0]` означает обращение к первому члену кортежа (то есть к `Tid`), а вы -ражение `msg[1]` – доступ к его второму члену (к целому числу). Наконец, -`writer` посылает уведомление о том, что завершила запись в консоль, по -просту отправляя собственный `Tid` отправителю предыдущего сообще -ния – своего рода пустой конверт, лишь подтверждающий личность от -правителя. «Да, я получил твое сообщение, – подразумевает пустое -письмо, – и принял соответствующие меры. Твоя очередь.» Основной -поток не продолжит работу, пока не получит такое уведомление, но как -только это произойдет, цикл начнет выполняться дальше. +Продолжим разбор `writer`. Следующая строка, собственно, выполняет печать (запись в консоль). Вспомните, что для кортежа `msg` выражение `msg[0]` означает обращение к первому члену кортежа (то есть к `Tid`), а выражение `msg[1]` – доступ к его второму члену (к целому числу). Наконец, `writer` посылает уведомление о том, что завершила запись в консоль, попросту отправляя собственный `Tid` отправителю предыдущего сообщения – своего рода пустой конверт, лишь подтверждающий личность отправителя. «Да, я получил твое сообщение, – подразумевает пустое письмо, – и принял соответствующие меры. Твоя очередь.» Основной поток не продолжит работу, пока не получит такое уведомление, но как только это произойдет, цикл начнет выполняться дальше. -Отправлять `Tid` дочернего потока назад в данном случае излишне – хва -тило бы любой болванки, например `int` или `bool`. Однако в общем случае -в программе есть много потоков, отправляющих друг другу сообщения, -так что самоидентификация становится важна. +Отправлять `Tid` дочернего потока назад в данном случае излишне – хватило бы любой болванки, например `int` или `bool`. Однако в общем случае в программе есть много потоков, отправляющих друг другу сообщения, так что самоидентификация становится важна. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-5-обмен-сообщениями-между-потоками) [Наверх ⮍](#13-параллельные-вычисления) ## 13.6. Сопоставление по шаблону с помощью receive -Большинство полезных протоколов взаимодействия сложнее, чем опре -деленный выше. Возможности, которые предоставляет `receiveOnly`, весь -ма ограничены. Например, с помощью `receiveOnly` довольно сложно реа -лизовать такой маневр, как «получить `int` или `string`». +Большинство полезных протоколов взаимодействия сложнее, чем определенный выше. Возможности, которые предоставляет `receiveOnly`, весьма ограничены. Например, с помощью `receiveOnly` довольно сложно реализовать такой маневр, как «получить `int` или `string`». -Гораздо более мощным примитивом является функция `receive`, которая -сопоставляет и диспетчирует сообщения в зависимости от их типа. Ти -пичный вызов `receive` выглядит так: +Гораздо более мощным примитивом является функция `receive`, которая сопоставляет и диспетчирует сообщения в зависимости от их типа. Типичный вызов `receive` выглядит так: ```d receive( @@ -751,8 +268,7 @@ receive( ); ``` -При сопоставлении этого вызова со следующими вызовами `send` во всех -случаях будет наблюдаться совпадение: +При сопоставлении этого вызова со следующими вызовами `send` во всех случаях будет наблюдаться совпадение: ```d send(tid, "здравствуй"); @@ -761,12 +277,7 @@ send(tid, 'a'); send(tid, 42u); ``` -Первый вызов `send` соответствует типу `string` и направляется в литерал -функции, определенный в `receive` первым; остальные три вызова соот -ветствуют типу `int` и передаются во второй функциональный литерал. -Кстати, в качестве функций-обработчиков необязательно использовать -литералы – какие-то (или даже все) обработчики могут быть адресами -именованных функций: +Первый вызов `send` соответствует типу `string` и направляется в литерал функции, определенный в `receive` первым; остальные три вызова соответствуют типу `int` и передаются во второй функциональный литерал. Кстати, в качестве функций-обработчиков необязательно использовать литералы – какие-то (или даже все) обработчики могут быть адресами именованных функций: ```d void handleString(string s) { ... } @@ -776,11 +287,7 @@ receive( ); ``` -Сопоставление не является досконально точным; вместо того чтобы тре -бовать точного совпадения, соблюдают обычные правила перегрузки, -в соответствии с которыми `char` и `uint` могут быть неявно преобразованы -в `int`. При сопоставлении следующих вызовов соответствие, напротив, -обнаружено *не будет*: +Сопоставление не является досконально точным; вместо того чтобы требовать точного совпадения, соблюдают обычные правила перегрузки, в соответствии с которыми `char` и `uint` могут быть неявно преобразованы в `int`. При сопоставлении следующих вызовов соответствие, напротив, обнаружено *не будет*: ```d send(tid, "hello"w); // Строка в кодировке UTF-16 (см. раздел 4.5) @@ -788,20 +295,9 @@ send(tid, 5L); // long send(tid, 42.0); // double ``` -Когда функция `receive` видит сообщение неожиданного типа, она не по -рождает исключение (как это делает `receiveOnly`). Подсистема обмена со -общениями просто сохраняет неподходящие сообщения в очереди, в на -роде называемой *почтовым ящиком* (*mailbox*) потока. `receive` терпели -во ждет, когда в почтовом ящике появится сообщение нужного типа. -Такая политика делает `receive` и протоколы, реализованные на базе -этой функции, более гибкими, но и более восприимчивыми к блокиро -ванию и переполнению ящика. Одно недоразумение при обмене инфор -мацией – и в ящике потока начнут накапливаться сообщения не тех -типов, а `receive` тем временем будет ждать сообщения, которое никогда -не придет. +Когда функция `receive` видит сообщение неожиданного типа, она не порождает исключение (как это делает `receiveOnly`). Подсистема обмена сообщениями просто сохраняет неподходящие сообщения в очереди, в народе называемой *почтовым ящиком* (*mailbox*) потока. `receive` терпеливо ждет, когда в почтовом ящике появится сообщение нужного типа. Такая политика делает `receive` и протоколы, реализованные на базе этой функции, более гибкими, но и более восприимчивыми к блокированию и переполнению ящика. Одно недоразумение при обмене информацией – и в ящике потока начнут накапливаться сообщения не тех типов, а `receive` тем временем будет ждать сообщения, которое никогда не придет. -Пользуясь посредническими услугами `Tuple`, дуэт `send`/`receive` с легко -стью обрабатывает и группы аргументов. Например: +Пользуясь посредническими услугами `Tuple`, дуэт `send`/`receive` с легкостью обрабатывает и группы аргументов. Например: ```d receive( @@ -819,14 +315,9 @@ receive( ); ``` -Такой вызов, как `send(tid, 5, 6.3)`, соответствует первому функциональ -ному литералу как первого, так и второго предыдущих примеров. +Такой вызов, как `send(tid, 5, 6.3)`, соответствует первому функциональному литералу как первого, так и второго предыдущих примеров. -Существует особая версия `receive` – функция `receiveTimeout`, позволяю -щая потоку предпринять экстренные меры в случае задержки сообще -ний. У `receiveTimeout` есть «срок годности»: она завершает свое выполне -ние по истечении указанного промежутка времени. Об истечении «от -пущенного времени» `receiveTimeout` сообщает, возвращая `false`: +Существует особая версия `receive` – функция `receiveTimeout`, позволяющая потоку предпринять экстренные меры в случае задержки сообщений. У `receiveTimeout` есть «срок годности»: она завершает свое выполнение по истечении указанного промежутка времени. Об истечении «отпущенного времени» `receiveTimeout` сообщает, возвращая `false`: ```d auto gotMessage = receiveTimeout( @@ -840,7 +331,7 @@ if (!gotMessage) { } ``` -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-6-сопоставление-по-шаблону-с-помощью-receive) [Наверх ⮍](#13-параллельные-вычисления) ### 13.6.1. Первое совпадение @@ -854,40 +345,19 @@ receive( ); ``` -Такой вызов не скомпилируется: `receive` отвергает этот вызов, посколь -ку третий обработчик недостижим при любых условиях. Любое отправ -ленное по каналу передачи значение типа `int` застревает в первом обра -ботчике. +Такой вызов не скомпилируется: `receive` отвергает этот вызов, поскольку третий обработчик недостижим при любых условиях. Любое отправленное по каналу передачи значение типа `int` застревает в первом обработчике. -Порядок аргументов `receive` определяет, каким образом осуществляют -ся попытки сопоставления. Принцип тот же, что и при вычислении бло -ков `catch` в инструкции `try`, но не при объектно-ориентированной диспет -черизации функций. Единого мнения насчет относительных преиму -ществ и недостатков использования первого совпадения или же лучше -го совпадения – нет; достаточно сказать, что, по всей видимости, первое -совпадение хорошо подходит для этого конкретного случая `receive`. +Порядок аргументов `receive` определяет, каким образом осуществляются попытки сопоставления. Принцип тот же, что и при вычислении блоков `catch` в инструкции `try`, но не при объектно-ориентированной диспетчеризации функций. Единого мнения насчет относительных преимуществ и недостатков использования первого совпадения или же лучшего совпадения – нет; достаточно сказать, что, по всей видимости, первое совпадение хорошо подходит для этого конкретного случая `receive`. -Выполнение принципа первого совпадения обеспечивается функцией -`receive` с помощью простого анализа, выполняемого во время компиля -ции. Для любых типов сообщения `‹Сбщ1›` и `‹Сбщ2›` справедливо, что, если -в вызове `receive` обработчик `‹Сбщ2›` следует после обработчика `‹Сбщ1›`, -`receive` гарантирует, что тип `‹Сбщ2›` *невозможно* неявно преобразовать -в тип `‹Сбщ1›`. Если можно, то это означает, что обработчик `‹Сбщ1›` будет ло -вить сообщения `‹Сбщ2›`, так что в компиляции такому вызову будет отка -зано. Выполнение этой проверки для предыдущего примера завершает -ся неудачей в процессе той итерации, когда `‹Сбщ1›` присваивается значе -ние `long`, а `‹Сбщ2›` – `int`. +Выполнение принципа первого совпадения обеспечивается функцией `receive` с помощью простого анализа, выполняемого во время компиляции. Для любых типов сообщения `‹Сбщ1›` и `‹Сбщ2›` справедливо, что, если в вызове `receive` обработчик `‹Сбщ2›` следует после обработчика `‹Сбщ1›`, `receive` гарантирует, что тип `‹Сбщ2›` *невозможно* неявно преобразовать в тип `‹Сбщ1›`. Если можно, то это означает, что обработчик `‹Сбщ1›` будет ловить сообщения `‹Сбщ2›`, так что в компиляции такому вызову будет отказано. Выполнение этой проверки для предыдущего примера завершается неудачей в процессе той итерации, когда `‹Сбщ1›` присваивается значение `long`, а `‹Сбщ2›` – `int`. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-6-1-первое-совпадение) [Наверх ⮍](#13-параллельные-вычисления) ### 13.6.2. Соответствие любому сообщению -Что если бы вы пожелали обеспечить просмотр абсолютно всех сообще -ний в почтовом ящике – например, для уверенности в том, что он не пе -реполнится мусором? +Что если бы вы пожелали обеспечить просмотр абсолютно всех сообщений в почтовом ящике – например, для уверенности в том, что он не переполнится мусором? -Ответ прост – нужно всего лишь включить обработчик сообщений типа -`Variant` последним в список аргументов `receive`. Например: +Ответ прост – нужно всего лишь включить обработчик сообщений типа `Variant` последним в список аргументов `receive`. Например: ```d receive( @@ -899,29 +369,15 @@ receive( ); ``` -Тип `Variant`, определенный в модуле `std.variant`, – это динамический -тип, вмещающий ровно одно значение любого другого типа. `receive` вос -принимает `Variant` как обобщенный контейнер для любого типа сообще -ния, а потому вызов `receive` с обработчиком для типа `Variant` всегда бу -дет отработан, если в очереди есть хотя бы одно сообщение. +Тип `Variant`, определенный в модуле `std.variant`, – это динамический тип, вмещающий ровно одно значение любого другого типа. `receive` воспринимает `Variant` как обобщенный контейнер для любого типа сообщения, а потому вызов `receive` с обработчиком для типа `Variant` всегда будет отработан, если в очереди есть хотя бы одно сообщение. -Расположить обработчик `Variant` в конце цепочки обработки сообще -ний – хороший способ избавить ваш почтовый ящик от случайных со -общений. +Расположить обработчик `Variant` в конце цепочки обработки сообщений – хороший способ избавить ваш почтовый ящик от случайных сообщений. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-6-2-соответствие-любому-сообщению) [Наверх ⮍](#13-параллельные-вычисления) ## 13.7. Копирование файлов – с выкрутасом -Напишем коротенькую программу для копирования файлов – один из -популярных способов познакомиться с интерфейсом языка файловой -системы. Классический пример в стиле Кернигана и Ричи целиком на -паре команд `getchar`/`putchar`! [34, глава 1, с. 15]. Конечно же, чтобы уско -рить передачу, «родные» программы системы, копирующие файлы, -практикуют буферное чтение и буферную запись, а также используют -множество других методов оптимизации, так что написать конкурен -тоспособную программу было бы сложно, однако параллельные вычис -ления нам помогут. +Напишем коротенькую программу для копирования файлов – один из популярных способов познакомиться с интерфейсом языка файловой системы. Классический пример в стиле Кернигана и Ричи целиком на паре команд `getchar`/`putchar`! [34, глава 1, с. 15]. Конечно же, чтобы ускорить передачу, «родные» программы системы, копирующие файлы, практикуют буферное чтение и буферную запись, а также используют множество других методов оптимизации, так что написать конкурентоспособную программу было бы сложно, однако параллельные вычисления нам помогут. Обычный способ копирования файлов: @@ -930,42 +386,17 @@ receive( 3. Записать данные из буфера в целевой файл. 4. Повторить заново, начиная с шага 1. -Добавление соответствующей обработки ошибок завершит полезную -(но не оригинальную) программу. Если размер буфера будет выбран дос -таточно большим, а оба файла (и источник, и целевой файл) окажутся -на одном и том же диске, быстродействие этого алгоритма приблизится -к оптимальному. +Добавление соответствующей обработки ошибок завершит полезную (но не оригинальную) программу. Если размер буфера будет выбран достаточно большим, а оба файла (и источник, и целевой файл) окажутся на одном и том же диске, быстродействие этого алгоритма приблизится к оптимальному. -В наше время файловыми хранилищами могут быть многие физические -устройства: жесткие диски, флеш-диски, оптические диски, подсоеди -ненные смартфоны, а также сетевые сервисы удаленного доступа. Эти -устройства характеризуются разнообразными показателями задержки -и скорости и подключаются с помощью разных аппаратных и программ -ных интерфейсов. Такие интерфейсы могут работать параллельно (а не -по одному в каждый момент времени, как предписывает алгоритм в сти -ле «прочесть данные из буфера/записать данные в буфер»), и именно -так и нужно их использовать. В идеале должна поддерживаться макси -мальная занятость как устройства-источника, так и устройства-полу -чателя, что мы можем изобразить как два потока, работающих по про -токолу «поставщик/потребитель»: +В наше время файловыми хранилищами могут быть многие физические устройства: жесткие диски, флеш-диски, оптические диски, подсоединенные смартфоны, а также сетевые сервисы удаленного доступа. Эти устройства характеризуются разнообразными показателями задержки и скорости и подключаются с помощью разных аппаратных и программных интерфейсов. Такие интерфейсы могут работать параллельно (а не по одному в каждый момент времени, как предписывает алгоритм в стиле «прочесть данные из буфера/записать данные в буфер»), и именно так и нужно их использовать. В идеале должна поддерживаться максимальная занятость как устройства-источника, так и устройства-получателя, что мы можем изобразить как два потока, работающих по протоколу «поставщик/потребитель»: -1. Породить один дочерний поток, который в цикле ждет сообщений, -содержащих буферы памяти, и записывает их в целевой файл. -2. Прочесть данные из исходного файла и разместить их в заново соз -данном буфере. +1. Породить один дочерний поток, который в цикле ждет сообщений, содержащих буферы памяти, и записывает их в целевой файл. +2. Прочесть данные из исходного файла и разместить их в заново созданном буфере. 3. Если ничего не было прочитано, копирование завершено. -4. Отправить дочернему потоку сообщение, содержащее буфер с прочи -танными данными. +4. Отправить дочернему потоку сообщение, содержащее буфер с прочитанными данными. 5. Повторить, начав с шага 2. -С таким подходом один поток будет работать с источником, а другой – -с приемником. В зависимости от природы «исходного пункта» и «пунк -та назначения» можно получить значительное ускорение. Если скоро -сти устройств сравнимы и невелики относительно пропускной способ -ности шины памяти, теоретически скорость копирования может быть -удвоена. Напишем простую программу, которая реализует модель «по -ставщик/потребитель» и копирует содержимое стандартного потока -ввода в стандартный поток вывода: +С таким подходом один поток будет работать с источником, а другой – с приемником. В зависимости от природы «исходного пункта» и «пункта назначения» можно получить значительное ускорение. Если скорости устройств сравнимы и невелики относительно пропускной способности шины памяти, теоретически скорость копирования может быть удвоена. Напишем простую программу, которая реализует модель «поставщик/потребитель» и копирует содержимое стандартного потока ввода в стандартный поток вывода: ```d import std.concurrency, std.stdio; @@ -992,101 +423,27 @@ void fileWriter() } ``` -В этой программе данные из основного потока передаются в дочерний -поток посредством разделения неизменяемых данных: передаваемые -сообщения имеют тип `immutable(ubyte)[]`, то есть являются массивами -неизменяемых значений типа `ubyte`. Эти буферы создаются в цикле -`foreach` при чтении данных из входного потока порциями, каждая из ко -торых имеет тип `immutable(ubyte)[]` и размер `bufferSize`. На каждом про -ходе цикла функция `byChunk` читает данные во временный буфер (пере -менную `buffer`), неизменная копия которого создается свойством `idup`. -Большую часть тяжелой работы выполняет управляющая часть `foreach`; -на долю тела этой конструкции остается лишь создание копии и от -правка буфера дочернему потоку. Как уже говорилось, передача данных -между потоками возможна благодаря присутствию квалификатора -`immutable`; если заменить `idup` на `dup`, вызов `send` не скомпилируется. +В этой программе данные из основного потока передаются в дочерний поток посредством разделения неизменяемых данных: передаваемые сообщения имеют тип `immutable(ubyte)[]`, то есть являются массивами неизменяемых значений типа `ubyte`. Эти буферы создаются в цикле `foreach` при чтении данных из входного потока порциями, каждая из которых имеет тип `immutable(ubyte)[]` и размер `bufferSize`. На каждом проходе цикла функция `byChunk` читает данные во временный буфер (переменную `buffer`), неизменная копия которого создается свойством `idup`. Большую часть тяжелой работы выполняет управляющая часть `foreach`; на долю тела этой конструкции остается лишь создание копии и отправка буфера дочернему потоку. Как уже говорилось, передача данных между потоками возможна благодаря присутствию квалификатора `immutable`; если заменить `idup` на `dup`, вызов `send` не скомпилируется. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-7-копирование-файлов-с-выкрутасом) [Наверх ⮍](#13-параллельные-вычисления) ## 13.8. Останов потока -В приводившихся до сих пор примерах есть кое-что необычное, в част -ности в функции `writer`, определенной в разделе 13.5, и в только что опре -деленной функции `fileWriter` из раздела 13.7: обе функции содержат -бесконечный цикл. На самом деле, повнимательнее взглянув на пример -с копированием файлов, можно заметить, что `main` и `fileWriter` прекрас -но понимают друг друга в разговоре о копировании, но никогда не обсу -ждают друг с другом останов приложения; другими словами, `main` нико -гда не говорит `fileWriter`: «Дело сделано, собирайся и пойдем домой». +В приводившихся до сих пор примерах есть кое-что необычное, в частности в функции `writer`, определенной в разделе 13.5, и в только что определенной функции `fileWriter` из раздела 13.7: обе функции содержат бесконечный цикл. На самом деле, повнимательнее взглянув на пример с копированием файлов, можно заметить, что `main` и `fileWriter` прекрасно понимают друг друга в разговоре о копировании, но никогда не обсуждают друг с другом останов приложения; другими словами, `main` никогда не говорит `fileWriter`: «Дело сделано, собирайся и пойдем домой». -Останов многопоточных приложений всегда был делом мудреным. По -ток легко запустить, но запустив, трудно остановить; завершение рабо -ты приложения – событие асинхронное и может застать приложение за -выполнением совершенно произвольной операции. Низкоуровневые API -для работы с потоками предоставляют средство для принудительного -останова потоков, неизменно сопровождая его предупреждением о чрез -мерной грубости этого инструмента и рекомендацией об использовании -какого-нибудь более высокоуровневого протокола завершения работы. +Останов многопоточных приложений всегда был делом мудреным. Поток легко запустить, но запустив, трудно остановить; завершение работы приложения – событие асинхронное и может застать приложение за выполнением совершенно произвольной операции. Низкоуровневые API для работы с потоками предоставляют средство для принудительного останова потоков, неизменно сопровождая его предупреждением о чрезмерной грубости этого инструмента и рекомендацией об использовании какого-нибудь более высокоуровневого протокола завершения работы. -D предоставляет простой и надежный протокол останова потоков. Каж -дый поток обладает *потоком-владельцем*; по умолчанию владельцем -считается поток, инициировавший вызов функции `spawn`. Владельца те -кущего потока можно изменить динамически, сделав вызов вида `setOwner(tid)`. У каждого потока только один владелец, но сам он может быть -владельцем множества потоков. +D предоставляет простой и надежный протокол останова потоков. Каждый поток обладает *потоком-владельцем*; по умолчанию владельцем считается поток, инициировавший вызов функции `spawn`. Владельца текущего потока можно изменить динамически, сделав вызов вида `setOwner(tid)`. У каждого потока только один владелец, но сам он может быть владельцем множества потоков. -Самое важное проявление отношения «владелец/собственность» за -ключается в том, что по завершении выполнения потока-владельца вы -зовы функции `receive` в дочернем потоке начнут порождать исключения -типа `OwnerTerminated`. Исключение порождается, только если в очереди -к `receive` больше нет подходящих сообщений и необходимо ждать при -хода новых; пока у `receive` есть что извлечь из ящика, она не породит -исключение `OwnerTerminated`. Другими словами, при останове потока- -владельца вызовы `receive` (или `receiveOnly`, коли на то пошло) в дочерних -потоках породят исключения тогда и только тогда, когда в противном -случае они заблокируют выполнение программы, так как продолжат -ожидать сообщение, которое никогда не придет. Отношение владения -необязательно однонаправленно. В действительности, возможна ситуа -ция, когда два потока являются владельцами друг друга; в таком слу -чае, какой бы поток ни завершался первым, он оповестит другой поток. +Самое важное проявление отношения «владелец/собственность» заключается в том, что по завершении выполнения потока-владельца вызовы функции `receive` в дочернем потоке начнут порождать исключения типа `OwnerTerminated`. Исключение порождается, только если в очереди к `receive` больше нет подходящих сообщений и необходимо ждать прихода новых; пока у `receive` есть что извлечь из ящика, она не породит исключение `OwnerTerminated`. Другими словами, при останове потока-владельца вызовы `receive` (или `receiveOnly`, коли на то пошло) в дочерних потоках породят исключения тогда и только тогда, когда в противном случае они заблокируют выполнение программы, так как продолжат ожидать сообщение, которое никогда не придет. Отношение владения необязательно однонаправленно. В действительности, возможна ситуация, когда два потока являются владельцами друг друга; в таком случае, какой бы поток ни завершался первым, он оповестит другой поток. -Окинем программу копирования файлов свежим взглядом – с учетом -знания об отношении владения. В любой заданный момент времени в по -лете между основным и второстепенным потоками находится масса сооб -щений. Чем быстрее выполняются операции чтения по сравнению с опе -рациями записи, тем больше буферов будет находиться в почтовом ящи -ке записывающего потока в ожидании обработки. Возврат из `main` заста -вит `receive` породить исключение, но не раньше, чем будут обработаны -ожидающие сообщения. Сразу же после того, как ящик записывающего -потока опустеет (а последняя порция данных будет записана в целевой -файл), очередной вызов `receive` породит исключение. Записывающий по -ток прекращает выполнение по исключению `OwnerTerminated`; система -времени исполнения в курсе, что это за исключение, и просто его игнори -рует. Операционная система закрывает стандартные потоки ввода и вы -вода так, как обычно, и операция копирования успешно завершается. +Окинем программу копирования файлов свежим взглядом – с учетом знания об отношении владения. В любой заданный момент времени в полете между основным и второстепенным потоками находится масса сообщений. Чем быстрее выполняются операции чтения по сравнению с операциями записи, тем больше буферов будет находиться в почтовом ящике записывающего потока в ожидании обработки. Возврат из `main` заставит `receive` породить исключение, но не раньше, чем будут обработаны ожидающие сообщения. Сразу же после того, как ящик записывающего потока опустеет (а последняя порция данных будет записана в целевой файл), очередной вызов `receive` породит исключение. Записывающий поток прекращает выполнение по исключению `OwnerTerminated`; система времени исполнения в курсе, что это за исключение, и просто его игнорирует. Операционная система закрывает стандартные потоки ввода и вывода так, как обычно, и операция копирования успешно завершается. -Может показаться, что в промежутке между моментом отправки по -следнего сообщения из `main` и моментом возврата из `main` (что заставляет -`receive` породить исключение) возникает гонка. Что если исключение -«опередит» последнее сообщение – или, хуже того, несколько послед -них сообщений? На самом деле никакой гонки нет. Поток, отправляю -щий сообщения, всегда думает о последствиях: последнее сообщение -помещается в конец очереди дочернего потока *до* того, как исключение -`OwnerTerminated` начнет свой путь (фактически распространение исклю -чения организуется при помощи той же очереди, что и в случае обыч -ных сообщений). Однако гонка *присутствовала бы*, если бы функция -`main` завершала свое выполнение в тот самый момент, когда другой, тре -тий поток отправлял бы сообщения в очередь `fileWriter`. +Может показаться, что в промежутке между моментом отправки последнего сообщения из `main` и моментом возврата из `main` (что заставляет `receive` породить исключение) возникает гонка. Что если исключение «опередит» последнее сообщение – или, хуже того, несколько последних сообщений? На самом деле никакой гонки нет. Поток, отправляющий сообщения, всегда думает о последствиях: последнее сообщение помещается в конец очереди дочернего потока *до* того, как исключение `OwnerTerminated` начнет свой путь (фактически распространение исключения организуется при помощи той же очереди, что и в случае обычных сообщений). Однако гонка *присутствовала бы*, если бы функция `main` завершала свое выполнение в тот самый момент, когда другой, третий поток отправлял бы сообщения в очередь `fileWriter`. -Подобная же цепочка рассуждений показывает, что наш предыдущий -простой пример, в котором два потока «в ногу» записывают 200 сообще -ний, также корректен: функция `main` завершает свое выполнение после -отправки (ждет до конца) последнего сообщения дочернему потоку. До -черний поток сначала опустошает очередь, а затем заканчивает работу -по исключению `OwnerTerminated`. +Подобная же цепочка рассуждений показывает, что наш предыдущий простой пример, в котором два потока «в ногу» записывают 200 сообщений, также корректен: функция `main` завершает свое выполнение после отправки (ждет до конца) последнего сообщения дочернему потоку. Дочерний поток сначала опустошает очередь, а затем заканчивает работу по исключению `OwnerTerminated`. -Если вы считаете, что для механизма, обрабатывающего завершение -выполнения потока, порождение исключения – выбор слишком суро -вый, то помните, что никто не лишал вас возможности обработать `OwnerTerminated` явно: +Если вы считаете, что для механизма, обрабатывающего завершение выполнения потока, порождение исключения – выбор слишком суровый, то помните, что никто не лишал вас возможности обработать `OwnerTerminated` явно: ```d // Завершается без исключения @@ -1104,144 +461,44 @@ void fileWriter() } ``` -В данном случае по завершении выполнения `main` поток `fileWriter` мирно -возвращает управление, и все счастливы. Но что произойдет, если ис -ключение породит дочерний, записывающий поток? Если возникнут -проблемы с записью данных в `tgt`, вызов функции `write` может завер -шиться неудачей. В таком случае вызов `send` из основного потока также -завершится неудачей (а именно будет порождено исключение типа `OwnerFailed`), то есть произойдет как раз то, что ожидалось. Кстати, если -дочерний поток завершит свое выполнение обычным способом (а не по -исключению), последующие вызовы `send`, отправлявшие сообщения это -му потоку, также завершатся неудачей, но с другим типом исключе -ния – `OwnedTerminated`. +В данном случае по завершении выполнения `main` поток `fileWriter` мирно возвращает управление, и все счастливы. Но что произойдет, если исключение породит дочерний, записывающий поток? Если возникнут проблемы с записью данных в `tgt`, вызов функции `write` может завершиться неудачей. В таком случае вызов `send` из основного потока также завершится неудачей (а именно будет порождено исключение типа `OwnerFailed`), то есть произойдет как раз то, что ожидалось. Кстати, если дочерний поток завершит свое выполнение обычным способом (а не по исключению), последующие вызовы `send`, отправлявшие сообщения этому потоку, также завершатся неудачей, но с другим типом исключения – `OwnedTerminated`. -Рассмотренная программа для копирования файлов более отказоустой -чива, чем можно предположить, судя по ее простоте. Тем не менее нуж -но сказать, что протокол завершения выполнения гладко срабатывает -лишь тогда, когда отношения между потоками просты и предельно по -нятны, и полагаться на него стоит исключительно в таких случаях. -А когда в деле замешаны несколько потоков и отношения владения ме -жду ними отражаются сложным графом, лучше всего организовать -взаимодействие всех этих потоков по протоколам, предусматривающим -явное уведомление об окончании обмена данными. В случае примера -с копированием файлов можно реализовать следующую простую идею: -установить соглашение, по которому отправка буфера нулевого размера -записывающему потоку будет означать удачное завершение работы чи -тающим потоком. Получив такое сообщение и завершив запись, запи -сывающий поток также уведомляет поток, осуществлявший чтение, -о своем завершении. После чего «читатель», наконец, тоже может за -вершить свое выполнение. Такой протокол явного уведомления хорошо -масштабируется до случаев, когда по пути от «читателя» к «писателю» -данные обрабатываются множеством других потоков. +Рассмотренная программа для копирования файлов более отказоустойчива, чем можно предположить, судя по ее простоте. Тем не менее нужно сказать, что протокол завершения выполнения гладко срабатывает лишь тогда, когда отношения между потоками просты и предельно понятны, и полагаться на него стоит исключительно в таких случаях. А когда в деле замешаны несколько потоков и отношения владения между ними отражаются сложным графом, лучше всего организовать взаимодействие всех этих потоков по протоколам, предусматривающим явное уведомление об окончании обмена данными. В случае примера с копированием файлов можно реализовать следующую простую идею: установить соглашение, по которому отправка буфера нулевого размера записывающему потоку будет означать удачное завершение работы читающим потоком. Получив такое сообщение и завершив запись, записывающий поток также уведомляет поток, осуществлявший чтение, о своем завершении. После чего «читатель», наконец, тоже может завершить свое выполнение. Такой протокол явного уведомления хорошо масштабируется до случаев, когда по пути от «читателя» к «писателю» данные обрабатываются множеством других потоков. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-8-останов-потока) [Наверх ⮍](#13-параллельные-вычисления) ## 13.9. Передача нештатных сообщений -Допустим, с помощью предположительно прыткой программы, кото -рую мы только что написали, вы копируете большой файл из быстрого -локального хранилища на медленный сетевой диск. На полпути возни -кает ошибка чтения – файл поврежден. Это заставляет `read`, а затем -и `main` породить исключения, и все происходит тогда, когда множество -буферов находятся в полете, но еще не записаны. Более абстрактно, мы -видели, что если поток-владелец завершит свое выполнение *обычным способом*, любой блокирующий вызов `receive` из принадлежащих ему -потоков породит исключение. Но что произойдет, если владелец завер -шит выполнение по исключению? +Допустим, с помощью предположительно прыткой программы, которую мы только что написали, вы копируете большой файл из быстрого локального хранилища на медленный сетевой диск. На полпути возникает ошибка чтения – файл поврежден. Это заставляет `read`, а затем и `main` породить исключения, и все происходит тогда, когда множество буферов находятся в полете, но еще не записаны. Более абстрактно, мы видели, что если поток-владелец завершит свое выполнение *обычным способом*, любой блокирующий вызов `receive` из принадлежащих ему потоков породит исключение. Но что произойдет, если владелец завершит выполнение по исключению? -Если поток завершается посредством порождения исключения, это знак -серьезной проблемы, о которой с должной настойчивостью нужно уве -домить дочерние потоки. И, конечно, это выполняется с помощью *нештатного* сообщения. +Если поток завершается посредством порождения исключения, это знак серьезной проблемы, о которой с должной настойчивостью нужно уведомить дочерние потоки. И, конечно, это выполняется с помощью *нештатного* сообщения. -Вспомните, что функция `receive` заботится лишь о сообщениях, совпав -ших с заданными шаблонами, а остальным позволяет накапливаться -в очереди. Есть способ внести в это поведение поправку. Поток-отправи -тель может инициировать обработку сообщения потоком-получателем, -вызвав функцию `prioritySend` вместо `send`. Эти две функции принимают -одни и те же параметры, но ведут себя по-разному, что в действительно -сти отражается на поведении получателя. Передача сообщения типа `T` -с помощью `prioritySend` заставляет `receive` в потоке-получателе действо -вать следующим образом: +Вспомните, что функция `receive` заботится лишь о сообщениях, совпавших с заданными шаблонами, а остальным позволяет накапливаться в очереди. Есть способ внести в это поведение поправку. Поток-отправитель может инициировать обработку сообщения потоком-получателем, вызвав функцию `prioritySend` вместо `send`. Эти две функции принимают одни и те же параметры, но ведут себя по-разному, что в действительности отражается на поведении получателя. Передача сообщения типа `T` с помощью `prioritySend` заставляет `receive` в потоке-получателе действовать следующим образом: -- Если вызов `receive` предусматривает обработку типа `T`, то сообщение -с приоритетом будет извлечено сразу же после завершения обработки -текущего сообщения – даже если сообщение с приоритетом пришло -позже других обычных (неприоритетных) сообщений. Сообщения -с приоритетом всегда помещаются в начало очереди, так что послед -нее пришедшее сообщение с приоритетом всегда извлекается функ -цией `receive` первым (даже если другие сообщения с приоритетом -уже ждут). -- Если вызов `receive` не обрабатывает тип `T` (то есть совокупность ука -занных обстоятельств предписывает `receive` оставить сообщение та -кого типа в почтовом ящике в ожидании) и `T` является наследником -`Exception`, то `receive` напрямую порождает извлеченное сообщение- -исключение. -- Если вызов `receive` не обрабатывает тип `T` и `T` не является наследни -ком `Exception`, то `receive` порождает исключение типа `PriorityMessageException!T`. Объект этого исключения содержит копию полученно -го сообщения в виде внутреннего элемента `message`. +- Если вызов `receive` предусматривает обработку типа `T`, то сообщение с приоритетом будет извлечено сразу же после завершения обработки текущего сообщения – даже если сообщение с приоритетом пришло позже других обычных (неприоритетных) сообщений. Сообщения с приоритетом всегда помещаются в начало очереди, так что последнее пришедшее сообщение с приоритетом всегда извлекается функцией `receive` первым (даже если другие сообщения с приоритетом уже ждут). +- Если вызов `receive` не обрабатывает тип `T` (то есть совокупность указанных обстоятельств предписывает `receive` оставить сообщение такого типа в почтовом ящике в ожидании) и `T` является наследником `Exception`, то `receive` напрямую порождает извлеченное сообщение-исключение. +- Если вызов `receive` не обрабатывает тип `T` и `T` не является наследником `Exception`, то `receive` порождает исключение типа `PriorityMessageException!T`. Объект этого исключения содержит копию полученного сообщения в виде внутреннего элемента `message`. -Если поток завершается по исключению, исключение `OwnerFailed` рас -пространяется на все потоки, которыми он владеет, с помощью вызова -`prioritySend`. В программе копирования файлов порождение исключе -ния внутри `main` вызывает порождение исключения и внутри `fileWriter` -(как только там будет вызвана функция `receive`); в результате, напеча -тав сообщение об ошибке и вернув ненулевой код выхода, останавлива -ется весь процесс. В отличие от случая с «нормальным» завершением -исполнения, в данной ситуации вполне допустимо, что в подвешенном -состоянии останутся буферы, которые были уже прочитаны, но еще не -записаны. +Если поток завершается по исключению, исключение `OwnerFailed` распространяется на все потоки, которыми он владеет, с помощью вызова `prioritySend`. В программе копирования файлов порождение исключения внутри `main` вызывает порождение исключения и внутри `fileWriter` (как только там будет вызвана функция `receive`); в результате, напечатав сообщение об ошибке и вернув ненулевой код выхода, останавливается весь процесс. В отличие от случая с «нормальным» завершением исполнения, в данной ситуации вполне допустимо, что в подвешенном состоянии останутся буферы, которые были уже прочитаны, но еще не записаны. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-9-передача-нештатных-сообщений) [Наверх ⮍](#13-параллельные-вычисления) ## 13.10. Переполнение почтового ящика -Программа для копирования файлов на основе протокола «поставщик/ -потребитель» работает достаточно хорошо, однако обладает одним важ -ным недостатком. Рассмотрим копирование большого файла, при кото -ром данные передаются между устройствами, скорость доступа к кото -рым существенно различается, например копирование приобретенного -законным способом файла с фильмом с внутреннего диска (быстрый дос -туп) на сетевой диск (вероятно, значительно более медленный доступ). -В этом случае поставщик (основной поток, выполняющий функцию -`main`) порождает буферы со значительной скоростью, гораздо более вы -сокой, чем скорость, с которой потребитель в состоянии записать их -в целевой файл. Разница в скоростях вызывает скопление данных, на -прасно занимающих память, которую программа не может использо -вать для повышения производительности. +Программа для копирования файлов на основе протокола «поставщик/потребитель» работает достаточно хорошо, однако обладает одним важным недостатком. Рассмотрим копирование большого файла, при котором данные передаются между устройствами, скорость доступа к которым существенно различается, например копирование приобретенного законным способом файла с фильмом с внутреннего диска (быстрый доступ) на сетевой диск (вероятно, значительно более медленный доступ). В этом случае поставщик (основной поток, выполняющий функцию `main`) порождает буферы со значительной скоростью, гораздо более высокой, чем скорость, с которой потребитель в состоянии записать их в целевой файл. Разница в скоростях вызывает скопление данных, напрасно занимающих память, которую программа не может использовать для повышения производительности. -Во избежание переполнения почтового ящика, API для параллельных -вычислений позволяет задать максимальный размер очереди сообще -ний, а также действие, предпринимаемое при достижении этого преде -ла. Соответствующие сигнатуры выглядят так: +Во избежание переполнения почтового ящика, API для параллельных вычислений позволяет задать максимальный размер очереди сообщений, а также действие, предпринимаемое при достижении этого предела. Соответствующие сигнатуры выглядят так: ```d // Внутри std.concurrency void setMaxMailboxSize(Tid tid, size_t messages, bool function(Tid) onCrowdingDoThis); ``` -Вызывая `setMailboxSize`, вы устанавливаете для подсистемы параллель -ных вычислений правило: всякий раз когда требуется отправить новое -сообщение, а очередь уже содержит число сообщений, указанное в `messages`, вызывать `onCrowdingDoThis(tid)`. Если `onCrowdingDoThis(tid)` возвра -щает `false` или порождает исключение, новое сообщение игнорируется. -В противном случае еще раз проверяется размер очереди потока, и если -выясняется, что он уже меньше, чем размер `messages`, новое сообщение -доставляется потоку с идентификатором `tid`. В противном случае весь -цикл возобновляется. +Вызывая `setMailboxSize`, вы устанавливаете для подсистемы параллельных вычислений правило: всякий раз когда требуется отправить новое сообщение, а очередь уже содержит число сообщений, указанное в `messages`, вызывать `onCrowdingDoThis(tid)`. Если `onCrowdingDoThis(tid)` возвращает `false` или порождает исключение, новое сообщение игнорируется. В противном случае еще раз проверяется размер очереди потока, и если выясняется, что он уже меньше, чем размер `messages`, новое сообщение доставляется потоку с идентификатором `tid`. В противном случае весь цикл возобновляется. -Вызов `setMaxMailboxSize` выполняется в потоке, осуществляющем вы -зов, а не в потоке, этот вызов принимающем. Иными словами, поток, -инициирующий отправку сообщения, также является ответственным -и за принятие экстренных мер при переполнении почтового ящика по -лучателя. Кажется логичным спросить: почему нельзя расположить -этот вызов в потоке-получателе? При расширении масштаба, а именно -применительно к программам с большим количеством потоков, такой -подход породил бы порочные последствия: потоки, пытающиеся отпра -вить сообщения, угрожали бы лишить трудоспособности потоки с пол -ными ящиками. +Вызов `setMaxMailboxSize` выполняется в потоке, осуществляющем вызов, а не в потоке, этот вызов принимающем. Иными словами, поток, инициирующий отправку сообщения, также является ответственным и за принятие экстренных мер при переполнении почтового ящика получателя. Кажется логичным спросить: почему нельзя расположить этот вызов в потоке-получателе? При расширении масштаба, а именно применительно к программам с большим количеством потоков, такой подход породил бы порочные последствия: потоки, пытающиеся отправить сообщения, угрожали бы лишить трудоспособности потоки с полными ящиками. -Есть ряд предопределенных действий, предпринимаемых в случае, ес -ли почтовый ящик полон: заблокировать отправителя до тех пор, пока -очередь не станет меньше, породить исключение или проигнорировать -новое сообщение. Такие предопределенные действия удобно упакованы: +Есть ряд предопределенных действий, предпринимаемых в случае, если почтовый ящик полон: заблокировать отправителя до тех пор, пока очередь не станет меньше, породить исключение или проигнорировать новое сообщение. Такие предопределенные действия удобно упакованы: ```d // Внутри std.concurrency @@ -1249,9 +506,7 @@ enum OnCrowding { block, throwException, ignore } void setMaxMailboxSize(Tid tid, size_t messages, OnCrowding doThis); ``` -В нашем случае лучше всего попросту блокировать поток-читатель, как -только ящик становится слишком большим. Добиться этого можно, -вставив вызов +В нашем случае лучше всего попросту блокировать поток-читатель, как только ящик становится слишком большим. Добиться этого можно, вставив вызов ```d setMaxMailboxSize(tid, 1024, OnCrowding.block); @@ -1259,25 +514,13 @@ setMaxMailboxSize(tid, 1024, OnCrowding.block); сразу же после вызова `spawn`. -В следующих разделах описываются подходы к организации межпоточ -ной передачи данных, служащие или альтернативой, или дополнением -к обмену сообщениями. Обмен сообщениями – рекомендуемый метод -организации межпоточного взаимодействия; этот метод легок для пони -мания, порождает удобный для чтения код, является надежным и мас -штабируемым. К более низкоуровневым механизмам стоит обращаться -лишь в совершенно особых обстоятельствах – и не забывайте, что «осо -бые» обстоятельства не всегда настолько особые, какими кажутся. +В следующих разделах описываются подходы к организации межпоточной передачи данных, служащие или альтернативой, или дополнением к обмену сообщениями. Обмен сообщениями – рекомендуемый метод организации межпоточного взаимодействия; этот метод легок для понимания, порождает удобный для чтения код, является надежным и масштабируемым. К более низкоуровневым механизмам стоит обращаться лишь в совершенно особых обстоятельствах – и не забывайте, что «особые» обстоятельства не всегда настолько особые, какими кажутся. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-10-переполнение-почтового-ящика) [Наверх ⮍](#13-параллельные-вычисления) ## 13.11. Квалификатор типа shared -Мы уже познакомились с квалификатором `shared` в разделе 13.3. Для -системы типов ключевое слово `shared` служит сигналом о том, что не -сколько потоков обладают доступом к одному фрагменту данных. Ком -пилятор тоже признает этот факт и соответственно реагирует, накла -дывая ограничения на операции с разделяемыми данными, а также -посредством генерации особого кода для разрешенных операций. +Мы уже познакомились с квалификатором `shared` в разделе 13.3. Для системы типов ключевое слово `shared` служит сигналом о том, что несколько потоков обладают доступом к одному фрагменту данных. Компилятор тоже признает этот факт и соответственно реагирует, накладывая ограничения на операции с разделяемыми данными, а также посредством генерации особого кода для разрешенных операций. С помощью глобального определения @@ -1285,12 +528,7 @@ setMaxMailboxSize(tid, 1024, OnCrowding.block); shared uint threadsCount; ``` -в программу на D вводится значение типа `shared(uint)`, что соответству -ет глобально определенному целому числу без знака в программе на C. -Такая переменная видима всем потокам в системе. Примечание в виде -shared здорово помогает компилятору: язык «знает», что `threadsCount` от -крыт для свободного доступа множеству потоков, и запрещает обраще -ния к этой переменной наивными способами. Например: +в программу на D вводится значение типа `shared(uint)`, что соответствует глобально определенному целому числу без знака в программе на C. Такая переменная видима всем потокам в системе. Примечание в виде `shared` здорово помогает компилятору: язык «знает», что `threadsCount` открыт для свободного доступа множеству потоков, и запрещает обращения к этой переменной наивными способами. Например: ```d void bumpThreadsCount() @@ -1299,15 +537,7 @@ void bumpThreadsCount() } ``` -Что происходит? Где-то внизу, на машинном уровне, `++threadCount` не яв -ляется атомарной операцией; это сложная операция, представляющая -собой последовательность трех простых: прочесть – изменить – запи -сать. Сначала `threadCount` загружается в регистр, затем значение регист -ра увеличивается на единицу и, наконец, `threadCount` записывается об -ратно в память. Для обеспечения корректности всей сложной операции -эти три шага необходимо выполнять единым блоком. Корректный спо -соб увеличить на единицу разделяемое целое число – воспользоваться -одним из специализированных атомарных примитивов из модуля `std.concurrency`: +Что происходит? Где-то внизу, на машинном уровне, `++threadCount` не является атомарной операцией; это сложная операция, представляющая собой последовательность трех простых: прочесть – изменить – записать. Сначала `threadCount` загружается в регистр, затем значение регистра увеличивается на единицу и, наконец, `threadCount` записывается обратно в память. Для обеспечения корректности всей сложной операции эти три шага необходимо выполнять единым блоком. Корректный способ увеличить на единицу разделяемое целое число – воспользоваться одним из специализированных атомарных примитивов из модуля `std.concurrency`: ```d import std.concurrency; @@ -1320,199 +550,77 @@ void bumpThreadsCount() } ``` -Поскольку все разделяемые данные тщательно учитываются и находят -ся под эгидой языка, передавать данные с квалификатором `shared` разре -шается с помощью функций `send` и `receive`. +Поскольку все разделяемые данные тщательно учитываются и находятся под эгидой языка, передавать данные с квалификатором `shared` разрешается с помощью функций `send` и `receive`. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-11-квалификатор-типа-shared) [Наверх ⮍](#13-параллельные-вычисления) ### 13.11.1. Сюжет усложняется: квалификатор shared транзитивен -В главе 8 объясняется, почему квалификаторы `const` и `immutable` долж -ны быть *транзитивными* (свойство, также известное как глубина или -рекурсивность): каким бы косвенным путем вы ни следовали, рассмат -ривая «внутренности» неизменяемого объекта, сами данные должны -оставаться неизменяемыми. В противном случае гарантии, предостав -ляемые квалификатором `immutable`, имели бы силу комментария в коде. -Нельзя сказать, что нечто «до определенного момента» неизменяемо -(`immutable`), а дальше меняется. Зато можно говорить, что данные *изменяемы* до определенного момента, а затем становятся совершенно неиз -меняемыми, вплоть до самых глубоко вложенных элементов. Применив -квалификатор `immutable`, вы сворачиваете на улицу с односторонним -движением. Мы уже видели, что присутствие квалификатора `immutable` -облегчает реализацию многих оправдавших себя идиом, не претендую -щих на свободу программиста, включая функциональный стиль и раз -деление данных между потоками. Если бы неизменяемость применя -лась «до определенного момента», то же самое относилось бы и к кор -ректности программы. +В главе 8 объясняется, почему квалификаторы `const` и `immutable` должны быть *транзитивными* (свойство, также известное как глубина или рекурсивность): каким бы косвенным путем вы ни следовали, рассматривая «внутренности» неизменяемого объекта, сами данные должны оставаться неизменяемыми. В противном случае гарантии, предоставляемые квалификатором `immutable`, имели бы силу комментария в коде. Нельзя сказать, что нечто «до определенного момента» неизменяемо (`immutable`), а дальше меняется. Зато можно говорить, что данные *изменяемы* до определенного момента, а затем становятся совершенно неизменяемыми, вплоть до самых глубоко вложенных элементов. Применив квалификатор `immutable`, вы сворачиваете на улицу с односторонним движением. Мы уже видели, что присутствие квалификатора `immutable` облегчает реализацию многих оправдавших себя идиом, не претендующих на свободу программиста, включая функциональный стиль и разделение данных между потоками. Если бы неизменяемость применялась «до определенного момента», то же самое относилось бы и к корректности программы. -Точно такой же ход рассуждений применим и для квалификатора `shared`. -На самом деле, в случае с `shared` необходимость транзитивности абсо -лютно очевидна. Приведем пример. Выражение +Точно такой же ход рассуждений применим и для квалификатора `shared`. На самом деле, в случае с `shared` необходимость транзитивности абсолютно очевидна. Приведем пример. Выражение ```d shared int* pInt; ``` -в соответствии с синтаксисом квалификаторов (см. раздел 8.2) эквива -лентно выражению +в соответствии с синтаксисом квалификаторов (см. раздел 8.2) эквивалентно выражению ```d shared(int*) pInt; ``` -Верная интерпретация `pInt` такова: «Указатель является разделяемым, -и данные, на которые он указывает, также разделяемы». При поверх -ностном, нетранзитивном подходе к разделению `pInt` превратился бы -в «разделяемый указатель на неразделяемую память», и все бы ничего, -если бы такой тип данных имел хоть какой-то смысл. Это все равно что -сказать: «Я делюсь этим бумажником со всеми; только, пожалуйста, не -забывайте, что деньгами из него я делиться не собирался»[^8]. Заявление, -что потоки разделяют указатель, но не данные, на которые он указыва -ет, возвращает нас к чудесной парадигме программирования на основе -системы доверия, которая всегда успешно проваливалась. И причина -большей части проблем не чьи-то происки, а честные ошибки. Про -граммное обеспечение имеет большой объем, сложно устроено и посто -янно изменяется, что плохо сочетается с обеспечением гарантий на ос -нове соглашений. +Верная интерпретация `pInt` такова: «Указатель является разделяемым, и данные, на которые он указывает, также разделяемы». При поверхностном, нетранзитивном подходе к разделению `pInt` превратился бы в «разделяемый указатель на неразделяемую память», и все бы ничего, если бы такой тип данных имел хоть какой-то смысл. Это все равно что сказать: «Я делюсь этим бумажником со всеми; только, пожалуйста, не забывайте, что деньгами из него я делиться не собирался»[^8]. Заявление, что потоки разделяют указатель, но не данные, на которые он указывает, возвращает нас к чудесной парадигме программирования на основе системы доверия, которая всегда успешно проваливалась. И причина большей части проблем не чьи-то происки, а честные ошибки. Программное обеспечение имеет большой объем, сложно устроено и постоянно изменяется, что плохо сочетается с обеспечением гарантий на основе соглашений. -Тем не менее есть совершенно логичное понятие «неразделяемый указа -тель на разделяемые данные». Некоторый поток обладает «личным» -указателем, а этот указатель «смотрит» на разделяемые данные. Эту -идею легко выразить синтаксически: +Тем не менее есть совершенно логичное понятие «неразделяемый указатель на разделяемые данные». Некоторый поток обладает «личным» указателем, а этот указатель «смотрит» на разделяемые данные. Эту идею легко выразить синтаксически: ```d shared(int)* pInt; ``` -Между нами, если бы существовала премия «За лучшее отображение со -держания», нотация `квалификатор(тип)` ее бы отхватила. Эта форма записи -совершенна. Синтаксис просто не позволит создать неправильный ука -затель. Некорректное сочетание синтаксических единиц выглядит так: +Между нами, если бы существовала премия «За лучшее отображение содержания», нотация `квалификатор(тип)` ее бы отхватила. Эта форма записи совершенна. Синтаксис просто не позволит создать неправильный указатель. Некорректное сочетание синтаксических единиц выглядит так: ```d int shared(*) pInt; ``` -Такое выражение не имеет смысла даже синтаксически, поскольку -`(*)` – это не тип (ну да, *на самом деле* этот милый смайлик символизиру -ет циклопа). +Такое выражение не имеет смысла даже синтаксически, поскольку `(*)` – это не тип (ну да, *на самом деле* этот милый смайлик символизирует циклопа). -Транзитивность квалификатора `shared` действует не только в отноше -нии указателей, но и в отношении полей объектов-структур и классов: -поля разделяемого объекта также автоматически воспринимаются как -помеченные квалификатором `shared`. Подробный разбор порядка взаи -модействия этого квалификатора с классами и структурами представ -лен далее в этой главе. +Транзитивность квалификатора `shared` действует не только в отношении указателей, но и в отношении полей объектов-структур и классов: поля разделяемого объекта также автоматически воспринимаются как помеченные квалификатором `shared`. Подробный разбор порядка взаимодействия этого квалификатора с классами и структурами представлен далее в этой главе. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-11-1-сюжет-усложняется-квалификатор-shared-транзитивен) [Наверх ⮍](#13-параллельные-вычисления) ## 13.12. Операции с разделяемыми данными и их применение -Работа с разделяемыми данными необычна, поскольку множество пото -ков могут читать и записывать разделяемые данные в любой момент. По -этому компилятор заботится о соблюдении целостности данных и при -чинности всеми операциями с разделяемыми данными. +Работа с разделяемыми данными необычна, поскольку множество потоков могут читать и записывать разделяемые данные в любой момент. Поэтому компилятор заботится о соблюдении целостности данных и причинности всеми операциями с разделяемыми данными. -Операции чтения и записи разделяемых (`shared`) значений разрешены, -и гарантированно будут атомарными для следующих типов: числовые -типы (кроме `real`), указатели, массивы, указатели на функции, делега -ты и ссылки на классы. Структуру с единственным полем одного из пе -речисленных типов также можно читать и записывать как неделимый -объект. Подчеркнутое отсутствие в списке «разрешенных типов» типа -`real` обусловлено тем, что это единственный тип, зависящий от платфор -мы. Вот почему в плане атомарного разделения компилятор смотрит на -`real` с опаской. На машинах Intel `real` занимает 80 бит, из-за чего пере -менным этого типа сложно делать атомарные присваивания в 32-раз -рядных программах. В любом случае, тип `real` предназначен для хране -ния временных результатов высокой точности, а не для обмена данны -ми, так что вряд ли у кого-то возникнет желание разделять значения -этого типа. +Операции чтения и записи разделяемых (`shared`) значений разрешены, и гарантированно будут атомарными для следующих типов: числовые типы (кроме `real`), указатели, массивы, указатели на функции, делегаты и ссылки на классы. Структуру с единственным полем одного из перечисленных типов также можно читать и записывать как неделимый объект. Подчеркнутое отсутствие в списке «разрешенных типов» типа `real` обусловлено тем, что это единственный тип, зависящий от платформы. Вот почему в плане атомарного разделения компилятор смотрит на `real` с опаской. На машинах Intel `real` занимает 80 бит, из-за чего переменным этого типа сложно делать атомарные присваивания в 32-разрядных программах. В любом случае, тип `real` предназначен для хранения временных результатов высокой точности, а не для обмена данными, так что вряд ли у кого-то возникнет желание разделять значения этого типа. -Для всех числовых типов и указателей на функции справедливо, что -значения этих типов с квалификатором `shared` могут неявно преобразо -вываться в значения без квалификатора и обратно. Преобразования -указателей между `shared(T*)` и `shared(T)*` разрешены в обоих направле -ниях. Арифметические операции на разделяемых числовых типах по -зволяют выполнять примитивы из модуля `std.concurrency`. +Для всех числовых типов и указателей на функции справедливо, что значения этих типов с квалификатором `shared` могут неявно преобразовываться в значения без квалификатора и обратно. Преобразования указателей между `shared(T*)` и `shared(T)*` разрешены в обоих направлениях. Арифметические операции на разделяемых числовых типах позволяют выполнять примитивы из модуля `std.concurrency`. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-12-операции-с-разделяемыми-данными-и-их-применение) [Наверх ⮍](#13-параллельные-вычисления) ### 13.12.1. Последовательная целостность разделяемых данных -Что касается видимости операций над разделяемыми данными между -потоками, D предоставляет следующие гарантии: -- порядок выполнения операций чтения и записи разделяемых дан -ных в рамках одного потока соответствует порядку, определенному -в исходном коде; -- глобальный порядок выполнения операций чтения и записи разде -ляемых данных представляет собой некоторое чередование опера -ций чтения и записи, выполнение которых инициируется из разных -потоков. +Что касается видимости операций над разделяемыми данными между потоками, D предоставляет следующие гарантии: +- порядок выполнения операций чтения и записи разделяемых данных в рамках одного потока соответствует порядку, определенному в исходном коде; +- глобальный порядок выполнения операций чтения и записи разделяемых данных представляет собой некоторое чередование операций чтения и записи, выполнение которых инициируется из разных потоков. -Выбор этих инвариантов кажется вполне резонным, даже очевидным. -И на самом деле такие гарантии довольно хорошо гармонируют с моде -лью вытесняющей многозадачности, реализованной на однопроцессор -ных системах. +Выбор этих инвариантов кажется вполне резонным, даже очевидным. И на самом деле такие гарантии довольно хорошо гармонируют с моделью вытесняющей многозадачности, реализованной на однопроцессорных системах. -Тем не менее в контексте мультипроцессорных систем такие гарантии -слишком строги. Проблема в следующем: для обеспечения этих гаран -тий необходимо сделать так, чтобы результат выполнения любой опера -ции записи был сразу же виден всем потокам. Единственный способ до -биться этого – окружить обращения к разделяемым данным особыми -машинными инструкциями (их называют *барьеры памяти*), которые -обеспечивали бы соответствие порядка, в котором выполняются опера -ции чтения и записи разделяемых данных, порядку обновления этих -данных в глазах всех запущенных потоков. Присутствие замыслова -тых иерархий кэшей значительно удорожает такую сериализацию. -Кроме того, непоколебимая приверженность принципу последователь -ной целостности заставляет отказаться от переупорядочивания опера -ций – основы множества способов оптимизации на уровне компилято -ра. В сочетании друг с другом эти два ограничения ведут к резкому за -медлению – вплоть до одного порядка единиц измерения. +Тем не менее в контексте мультипроцессорных систем такие гарантии слишком строги. Проблема в следующем: для обеспечения этих гарантий необходимо сделать так, чтобы результат выполнения любой операции записи был сразу же виден всем потокам. Единственный способ добиться этого – окружить обращения к разделяемым данным особыми машинными инструкциями (их называют *барьеры памяти*), которые обеспечивали бы соответствие порядка, в котором выполняются операции чтения и записи разделяемых данных, порядку обновления этих данных в глазах всех запущенных потоков. Присутствие замысловатых иерархий кэшей значительно удорожает такую сериализацию. Кроме того, непоколебимая приверженность принципу последовательной целостности заставляет отказаться от переупорядочивания операций – основы множества способов оптимизации на уровне компилятора. В сочетании друг с другом эти два ограничения ведут к резкому замедлению – вплоть до одного порядка единиц измерения. -Хорошая новость заключается в том, что такая потеря скорости имеет -место лишь в отношении разделяемых данных, которые используются -достаточно редко. В реальных ситуациях большинство данных не раз -деляются, а потому нет необходимости, чтобы в их отношении соблю -дался принцип последовательной целостности. Компилятор оптимизи -рует код, используя неразделяемые данные на всю катушку, в полной -уверенности, что другой поток никогда к ним не обратится, и относится -с осторожностью лишь к разделяемым данным. Повсеместно использу -емый и рекомендуемый прием для работы с разделяемыми данными – -копировать значения разделяемых переменных в локальные рабочие -копии потоков, работать с копиями и затем присваивать копии тем же -разделяемым переменным. +Хорошая новость заключается в том, что такая потеря скорости имеет место лишь в отношении разделяемых данных, которые используются достаточно редко. В реальных ситуациях большинство данных не разделяются, а потому нет необходимости, чтобы в их отношении соблюдался принцип последовательной целостности. Компилятор оптимизирует код, используя неразделяемые данные на всю катушку, в полной уверенности, что другой поток никогда к ним не обратится, и относится с осторожностью лишь к разделяемым данным. Повсеместно используемый и рекомендуемый прием для работы с разделяемыми данными – копировать значения разделяемых переменных в локальные рабочие копии потоков, работать с копиями и затем присваивать копии тем же разделяемым переменным. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-12-1-последовательная-целостность-разделяемых-данных) [Наверх ⮍](#13-параллельные-вычисления) ## 13.13. Синхронизация на основе блокировок через синхронизированные классы -Традиционно популярный метод многопоточного программирования – -*синхронизация на основе блокировок*. В соответствии с этим подходом -разделяемые данные защищаются с помощью мьютексов – объектов -синхронизации, обеспечивающих переход от параллельного к последо -вательному исполнению фрагментов кода, которые или временно нару -шают когерентность данных, или могут видеть эти временные наруше -ния. Такие фрагменты кода называют *критическими участками*[^9]. +Традиционно популярный метод многопоточного программирования – *синхронизация на основе блокировок*. В соответствии с этим подходом разделяемые данные защищаются с помощью мьютексов – объектов синхронизации, обеспечивающих переход от параллельного к последовательному исполнению фрагментов кода, которые или временно нарушают когерентность данных, или могут видеть эти временные нарушения. Такие фрагменты кода называют *критическими участками*[^9]. -Корректность программы, основанной на блокировках, обеспечивается -за счет ввода упорядоченного, последовательного доступа к разделяе -мым данным. Поток, которому требуется обратиться к фрагменту раз -деляемых данных, должен захватить (заблокировать) мьютекс, обрабо -тать данные, а затем освободить (разблокировать) мьютекс. В любой за -данный момент времени мьютексом может обладать только один поток, -благодаря чему и обеспечивается переход к последовательному выпол -нению: если захватить один и тот же мьютекс желают несколько пото -ков, то «выигрывает» лишь один, а остальные скромно ожидают своей -очереди. (Способ обслуживания очереди, то есть порядок очередности, -играет важную роль и может довольно заметно сказываться на работе -приложений и операционной системы.) +Корректность программы, основанной на блокировках, обеспечивается за счет ввода упорядоченного, последовательного доступа к разделяемым данным. Поток, которому требуется обратиться к фрагменту разделяемых данных, должен захватить (заблокировать) мьютекс, обработать данные, а затем освободить (разблокировать) мьютекс. В любой заданный момент времени мьютексом может обладать только один поток, благодаря чему и обеспечивается переход к последовательному выполнению: если захватить один и тот же мьютекс желают несколько потоков, то «выигрывает» лишь один, а остальные скромно ожидают своей очереди. (Способ обслуживания очереди, то есть порядок очередности, играет важную роль и может довольно заметно сказываться на работе приложений и операционной системы.) -По всей вероятности, «Здравствуй, мир!» многопоточного программи -рования – это пример с банковским счетом: объект, доступный множе -ству потоков, должен предоставить безопасный интерфейс для пополне -ния счета и извлечения денежных средств со счета. Вот однопоточная, -базовая версия программы, позволяющей выполнять эти действия: +По всей вероятности, «Здравствуй, мир!» многопоточного программирования – это пример с банковским счетом: объект, доступный множеству потоков, должен предоставить безопасный интерфейс для пополнения счета и извлечения денежных средств со счета. Вот однопоточная, базовая версия программы, позволяющей выполнять эти действия: ```d import std.contracts; @@ -1537,30 +645,9 @@ class BankAccount } ``` -В отсутствие потоков операции `+=` и `-=` слегка вводят в заблуждение: они -«выглядят» как атомарные, но таковыми не являются – обе операции -состоят из тройки простых операций «прочесть – изменить – записать». -На самом деле, выражение `_balance += amount` кодируется как `_balance = _balance + amount`, а значит, процессор загружает `_balance` и `_amount` в соб -ственную оперативную память (регистры или внутренний стек), скла -дывает их, а затем переводит результат обратно в `_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`: +Незащищенные параллельные операции типа «прочесть – изменить – записать» становятся причиной некорректного поведения программы. Скажем, баланс вашего счета характеризует истинное выражение `_balance == 100.0`. Некоторый поток, запуск которого был спровоцирован требованием зачислить денежные средства по чеку, делает вызов `deposit(50)`. Сразу же после загрузки из памяти значения `100.0` выполнение этой операции прерывает другой поток, осуществляющий вызов `withdraw(2.5)`. (Это вы в кофейне на углу оплачиваете латте своей дебетовой картой.) Пусть ничто не вклинивается в обработку этого вызова, так что поток, запущенный из кофейни, удачно обновляет поле `_balance`, и оно принимает значение `97.5`. Однако это событие происходит совершенно без ведома депонирующего потока, который уже загрузил число `100` в регистр ЦПУ и все еще считает, что это верное количество. При вычислении нового значения баланса вызов `deposit(50)` получает `150` и записывает это число назад в переменную `_balance`. Это типичное *состояние гонки*. Поздравляю, вы получили бесплатный кофе (но остерегайтесь: книжные примеры с ошибками еще могут работать на вас, а готовый код с ошибками – нет). Для организации корректной синхронизации многие языки предоставляют специальное средство – тип `Mutex`, который используется в программах, работающих с несколькими потоками на основе блокировок, для защиты доступа к `balance`: ```d // Этот код написан не на D @@ -1601,45 +688,9 @@ class BankAccount } ``` -Все операции над `_balance` теперь защищены, поскольку для доступа -к этому полю необходимо заполучить `_guard`. Может показаться, что при -ставлять к `_balance` охранника в виде `_guard` излишне, так как значения -типа `double` можно читать и записывать «в один присест», однако защита -должна здесь присутствовать по причинам, скрытым многочисленны -ми завесами майи. Вкратце, из-за сегодняшних агрессивно оптимизи -рующих компиляторов и нестрогих моделей памяти *любое* обращение -к разделяемым данным должно сопровождаться своего рода секрет -ным соглашением между записывающим потоком, читающим потоком -и оптимизирующим компилятором; одно неосторожное чтение разде -ляемых данных – и вы оказываетесь в мире боли (хорошо, что D наме -ренно запрещает такую «наготу»). Первая и наиболее очевидная при -чина такого положения дел в том, что оптимизирующий компилятор, -не замечая каких-либо попыток синхронизировать доступ к данным -с вашей стороны, ощущает себя вправе оптимизировать код с обраще -ниями к `_balance`, удерживая значение этого поля в регистре. Вторая -причина в том, что во всех случаях, кроме самых тривиальных, компи -лятор *и* ЦПУ ощущают себя вправе свободно переупорядочивать неза -щищенные, не снабженные никаким дополнительным описанием обра -щения к разделяемым данным, поскольку считают, что имеют дело -с данными, принадлежащими лично одному потоку. (Почему? Да пото -му что чаще всего так и бывает, оптимизация порождает код с самым -высоким быстродействием, и в конце концов, почему должны страдать -плебеи, а не избранные и достойные?) Это один из тех моментов, с помо -щью которых современная многопоточность выражает свое пренебре -жение к интуиции и сбивает с толку программистов, сведущих в клас -сической многопоточности. Короче, чтобы обеспечить заключение сек -ретного соглашения, потребуется обязательно синхронизировать обра -щения к свойству `_balance`. +Все операции над `_balance` теперь защищены, поскольку для доступа к этому полю необходимо заполучить `_guard`. Может показаться, что приставлять к `_balance` охранника в виде `_guard` излишне, так как значения типа `double` можно читать и записывать «в один присест», однако защита должна здесь присутствовать по причинам, скрытым многочисленными завесами майи. Вкратце, из-за сегодняшних агрессивно оптимизирующих компиляторов и нестрогих моделей памяти *любое* обращение к разделяемым данным должно сопровождаться своего рода секретным соглашением между записывающим потоком, читающим потоком и оптимизирующим компилятором; одно неосторожное чтение разделяемых данных – и вы оказываетесь в мире боли (хорошо, что D намеренно запрещает такую «наготу»). Первая и наиболее очевидная причина такого положения дел в том, что оптимизирующий компилятор, не замечая каких-либо попыток синхронизировать доступ к данным с вашей стороны, ощущает себя вправе оптимизировать код с обращениями к `_balance`, удерживая значение этого поля в регистре. Вторая причина в том, что во всех случаях, кроме самых тривиальных, компилятор *и* ЦПУ ощущают себя вправе свободно переупорядочивать незащищенные, не снабженные никаким дополнительным описанием обращения к разделяемым данным, поскольку считают, что имеют дело с данными, принадлежащими лично одному потоку. (Почему? Да потому что чаще всего так и бывает, оптимизация порождает код с самым высоким быстродействием, и в конце концов, почему должны страдать плебеи, а не избранные и достойные?) Это один из тех моментов, с помощью которых современная многопоточность выражает свое пренебрежение к интуиции и сбивает с толку программистов, сведущих в классической многопоточности. Короче, чтобы обеспечить заключение секретного соглашения, потребуется обязательно синхронизировать обращения к свойству `_balance`. -Чтобы гарантировать корректное снятие блокировки с `Mutex` в условиях -возникновения исключений и преждевременных возвратов управле -ния, языки, в которых продолжительность жизни объектов контекстно -ограничена (то есть деструкторы объектов вызываются на выходе из об -ластей видимости этих объектов), определяют вспомогательный тип -`Lock`, который устанавливает блок в конструкторе и снимает его в де -структоре. Эта идея развилась в самостоятельную идиому, известную -как *контекстное блокирование*. Приложение этой идиомы к клас -су `BankAccount` выглядит так: +Чтобы гарантировать корректное снятие блокировки с `Mutex` в условиях возникновения исключений и преждевременных возвратов управления, языки, в которых продолжительность жизни объектов контекстно ограничена (то есть деструкторы объектов вызываются на выходе из областей видимости этих объектов), определяют вспомогательный тип `Lock`, который устанавливает блок в конструкторе и снимает его в деструкторе. Эта идея развилась в самостоятельную идиому, известную как *контекстное блокирование*. Приложение этой идиомы к классу `BankAccount` выглядит так: ```d // Версия C++: банковский счет, защищенный методом контекстного блокирования @@ -1668,13 +719,7 @@ public: } ``` -Благодаря введению типа `Lock` код упрощается и повышается его кор -ректность: ведь соблюдение парности операций установления и снятия -блока теперь гарантировано, поскольку они выполняются автоматиче -ски. Java, C# и другие языки еще сильнее упрощают работу с блоки -ровками, встраивая `_guard` в объекты в качестве скрытого внутреннего -элемента и приподнимая логику блокирования вверх, до уровня сигна -туры метода. Наш пример, реализованный на Java, выглядел бы так: +Благодаря введению типа `Lock` код упрощается и повышается его корректность: ведь соблюдение парности операций установления и снятия блока теперь гарантировано, поскольку они выполняются автоматически. Java, C# и другие языки еще сильнее упрощают работу с блокировками, встраивая `_guard` в объекты в качестве скрытого внутреннего элемента и приподнимая логику блокирования вверх, до уровня сигнатуры метода. Наш пример, реализованный на Java, выглядел бы так: ```d // Версия Java: банковский счет, защищенный методом контекстного @@ -1698,28 +743,11 @@ class BankAccount } ``` -Соответствующий код на C# выглядит так же, за исключением того, -что ключевое слово `synchronized` должно быть заменено на `[MethodImpl(MethodImplOptions.Synchronized)]`. +Соответствующий код на C# выглядит так же, за исключением того, что ключевое слово `synchronized` должно быть заменено на `[MethodImpl(MethodImplOptions.Synchronized)]`. -Итак, вы только что узнали хорошую новость: небольшие программы, -основанные на блокировках, легки для понимания и, кажется, неплохо -работают. Плохая новость в том, что при большом масштабе очень слож -но сопоставлять должным образом блокировки и данные, выбирать -контекст и «калибр» блокирования и последовательно устанавливать -блокировки, затрагивающие сразу несколько объектов (не говоря о том, -что последнее может привести к тому, что взаимозаблокированные по -токи, ожидая завершения работы друг друга, попадают в *тупик*). В ста -рые добрые времена классического многопоточного программирования -подобные проблемы весьма осложняли кодирование на основе блокиро -вок; современная многопоточность (ориентированная на множество про -цессоров, с нестрогими моделями памяти и дорогим разделением дан -ных) поставила практику программирования с блокировками под удар. Тем не менее синхронизация на основе блокировок все еще полезна -для реализации множества задумок. +Итак, вы только что узнали хорошую новость: небольшие программы, основанные на блокировках, легки для понимания и, кажется, неплохо работают. Плохая новость в том, что при большом масштабе очень сложно сопоставлять должным образом блокировки и данные, выбирать контекст и «калибр» блокирования и последовательно устанавливать блокировки, затрагивающие сразу несколько объектов (не говоря о том, что последнее может привести к тому, что взаимозаблокированные потоки, ожидая завершения работы друг друга, попадают в *тупик*). В старые добрые времена классического многопоточного программирования подобные проблемы весьма осложняли кодирование на основе блокировок; современная многопоточность (ориентированная на множество процессоров, с нестрогими моделями памяти и дорогим разделением данных) поставила практику программирования с блокировками под удар. Тем не менее синхронизация на основе блокировок все еще полезна для реализации множества задумок. -Для организации синхронизации с помощью блокировок D предостав -ляет лишь ограниченные средства. Эти границы установлены намерен -но: преимущество в том, что таким образом обеспечиваются серьезные -гарантии. Что касается случая с `BankAccount`, версия D очень проста: +Для организации синхронизации с помощью блокировок D предоставляет лишь ограниченные средства. Эти границы установлены намеренно: преимущество в том, что таким образом обеспечиваются серьезные гарантии. Что касается случая с `BankAccount`, версия D очень проста: ```d // Версия D: банковский счет, реализованный с помощью синхронизированного класса @@ -1742,73 +770,29 @@ synchronized class BankAccount } ``` -D поднимает ключевое слово `synchronized` на один уровень выше, так -чтобы оно применялось к целому классу[^10]. Благодаря этому маневру -класс `BankAccount`, реализованный на D, получает возможность предос -тавлять более серьезные гарантии: даже если бы вы пожелали совер -шить ошибку, при таком синтаксисе нет способа оставить открытой -хоть какую-нибудь дверь с черного хода для несинхронизированных -обращений к `_balance`. Если бы в D позволялось смешивать использова -ние синхронизированных и несинхронизированных методов в рамках -одного класса, все обещания, данные синхронизированными метода -ми, оказались бы нарушенными. На самом деле опыт с синхронизацией -на уровне методов показал, что лучше всего синхронизировать или все -методы, или ни один из них; классы двойного назначения приносят -больше проблем, чем удобств. +D поднимает ключевое слово `synchronized` на один уровень выше, так чтобы оно применялось к целому классу[^10]. Благодаря этому маневру класс `BankAccount`, реализованный на D, получает возможность предоставлять более серьезные гарантии: даже если бы вы пожелали совершить ошибку, при таком синтаксисе нет способа оставить открытой хоть какую-нибудь дверь с черного хода для несинхронизированных обращений к `_balance`. Если бы в D позволялось смешивать использование синхронизированных и несинхронизированных методов в рамках одного класса, все обещания, данные синхронизированными методами, оказались бы нарушенными. На самом деле опыт с синхронизацией на уровне методов показал, что лучше всего синхронизировать или все методы, или ни один из них; классы двойного назначения приносят больше проблем, чем удобств. -Объявляемый на уровне класса атрибут `synchronized` действует на объек -ты типа `shared(BankAccount)` и автоматически превращает параллельное -выполнение вызовов любых методов класса в последовательное. Кроме -того, синхронизированные классы характеризуются возросшей строго -стью проверок уровня защиты их внутренних элементов. Вспомните, -в соответствии с разделом 11.1 обычные проверки уровня защиты в об -щем случае позволяют обращаться к любым не общедоступным (`public`) -внутренним элементам модуля любому коду внутри этого модуля. Толь -ко не в случае синхронизированных классов – классы с атрибутом `synchronized` подчиняются следующим правилам: +Объявляемый на уровне класса атрибут `synchronized` действует на объекты типа `shared(BankAccount)` и автоматически превращает параллельное выполнение вызовов любых методов класса в последовательное. Кроме того, синхронизированные классы характеризуются возросшей строгостью проверок уровня защиты их внутренних элементов. Вспомните, в соответствии с разделом 11.1 обычные проверки уровня защиты в общем случае позволяют обращаться к любым не общедоступным (`public`) внутренним элементам модуля любому коду внутри этого модуля. Только не в случае синхронизированных классов – классы с атрибутом `synchronized` подчиняются следующим правилам: - объявлять общедоступные (`public`) данные и вовсе запрещено; -- право доступа к защищенным (`protected`) внутренним элементам -есть только у методов текущего класса и его потомков; -- право доступа к закрытым (`private`) внутренним элементам есть толь -ко у методов текущего класса. +- право доступа к защищенным (`protected`) внутренним элементам есть только у методов текущего класса и его потомков; +- право доступа к закрытым (`private`) внутренним элементам есть только у методов текущего класса. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-13-синхронизация-на-основе-блокировок-через-синхронизированные-классы) [Наверх ⮍](#13-параллельные-вычисления) ## 13.14. Типизация полей в синхронизированных классах -В соответствии с правилом транзитивности для разделяемых (`shared`) -объектов разделяемый объект класса распространяет квалификатор -`shared` на свои поля. Очевидно, что атрибут `synchronized` привносит неко -торый дополнительный закон и порядок, что отражается в нестрогой -проверке типов полей внутри методов синхронизированных классов. -Ключевое слово `synchronized` должно предоставлять серьезные гарантии, -поэтому его присутствие своеобразно отражается на семантической про -верке полей, в чем прослеживается настолько же своеобразная семанти -ка самого атрибута `synchronized`. +В соответствии с правилом транзитивности для разделяемых (`shared`) объектов разделяемый объект класса распространяет квалификатор `shared` на свои поля. Очевидно, что атрибут `synchronized` привносит некоторый дополнительный закон и порядок, что отражается в нестрогой проверке типов полей внутри методов синхронизированных классов. Ключевое слово `synchronized` должно предоставлять серьезные гарантии, поэтому его присутствие своеобразно отражается на семантической проверке полей, в чем прослеживается настолько же своеобразная семантика самого атрибута `synchronized`. -Защита синхронизированных методов от гонок *временна* и *локальна*. -Свойство временности означает, что как только метод возвращает управ -ление, поля от гонок больше не защищаются. Свойство локальности -подразумевает, что `synchronized` обеспечивает защиту данных, встроен -ных непосредственно в объект, но не данных, на которые объект ссыла -ется косвенно (то есть через ссылки на классы, указатели или массивы). -Рассмотрим каждое из этих свойств по очереди. +Защита синхронизированных методов от гонок *временна* и *локальна*. Свойство временности означает, что как только метод возвращает управление, поля от гонок больше не защищаются. Свойство локальности подразумевает, что `synchronized` обеспечивает защиту данных, встроенных непосредственно в объект, но не данных, на которые объект ссылается косвенно (то есть через ссылки на классы, указатели или массивы). Рассмотрим каждое из этих свойств по очереди. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-14-типизация-полей-в-синхронизированных-классах) [Наверх ⮍](#13-параллельные-вычисления) ### 13.14.1. Временная защита == нет утечкам -Возможно, это не вполне очевидно, но «побочным эффектом» времен -ной природы `synchronized` становится формирование следующего пра -вила: ни один адрес поля не в состоянии «утечь» из синхронизирован -ного кода. Если бы такое произошло, некоторый другой фрагмент кода -получил бы право доступа к некоторым данным за пределами времен -ной защиты, даруемой синхронизацией на уровне методов. +Возможно, это не вполне очевидно, но «побочным эффектом» временной природы `synchronized` становится формирование следующего правила: ни один адрес поля не в состоянии «утечь» из синхронизированного кода. Если бы такое произошло, некоторый другой фрагмент кода получил бы право доступа к некоторым данным за пределами временной защиты, даруемой синхронизацией на уровне методов. -Компилятор пресечет любые поползновения возвратить из метода ссыл -ку или указатель на поле или передать значение поля по ссылке или по -указателю в некоторую функцию. Покажем, в чем смысл этого прави -ла, на следующем примере[^11]: +Компилятор пресечет любые поползновения возвратить из метода ссылку или указатель на поле или передать значение поля по ссылке или по указателю в некоторую функцию. Покажем, в чем смысл этого правила, на следующем примере[^11]: ```d double * nyukNyuk; // Обратите внимание: без shared @@ -1826,55 +810,22 @@ synchronized class BankAccount } ``` -В первой строке `fun` осуществляется попытка получить адрес `_balance` -и присвоить его глобальной переменной. Если бы эта операция заверши -лась успехом, гарантии системы типов превратились бы в ничто – с мо -мента «утечки» адреса появилась бы возможность обращаться к разде -ляемым данным через неразделяемое значение. Присваивание не прохо -дит проверку типов. Вторая операция чуть более коварна в том смысле, -что предпринимает попытку создать псевдоним более изощренным спо -собом – через вызов функции, принимающей параметр по ссылке. Та -кое тоже не проходит; передача значения с помощью `ref` фактически -влечет получение адреса до совершения вызова. Операция получения -адреса запрещена, так что и вызов завершается неудачей. +В первой строке `fun` осуществляется попытка получить адрес `_balance` и присвоить его глобальной переменной. Если бы эта операция завершилась успехом, гарантии системы типов превратились бы в ничто – с момента «утечки» адреса появилась бы возможность обращаться к разделяемым данным через неразделяемое значение. Присваивание не проходит проверку типов. Вторая операция чуть более коварна в том смысле, что предпринимает попытку создать псевдоним более изощренным способом – через вызов функции, принимающей параметр по ссылке. Такое тоже не проходит; передача значения с помощью `ref` фактически влечет получение адреса до совершения вызова. Операция получения адреса запрещена, так что и вызов завершается неудачей. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-14-1-временная-защита-нет-утечкам) [Наверх ⮍](#13-параллельные-вычисления) ### 13.14.2. Локальная защита == разделение хвостов -Защита, которую предоставляет `synchronized`, обладает еще одним важ -ным качеством – она локальна. Имеется в виду, что она не обязательно -распространяется на какие-либо данные помимо непосредственных по -лей объекта. Как только на горизонте появляются косвенности, гаран -тия того, что потоки будут обращаться к данным по одному, практиче -ски утрачивается. Если считать, что данные состоят из «головы» (часть, -расположенная в физической памяти, которую занимает объект клас -са `BankAccount`) и, возможно, «хвоста» (косвенно доступная память), то -можно сказать, что синхронизированный класс в состоянии защитить -лишь «голову» данных, в то время как «хвост» остается разделяемым -(`shared`). По этой причине типизация полей синхронизированного (`synchronized`) класса внутри метода выполняется особым образом: +Защита, которую предоставляет `synchronized`, обладает еще одним важным качеством – она локальна. Имеется в виду, что она не обязательно распространяется на какие-либо данные помимо непосредственных полей объекта. Как только на горизонте появляются косвенности, гарантия того, что потоки будут обращаться к данным по одному, практически утрачивается. Если считать, что данные состоят из «головы» (часть, расположенная в физической памяти, которую занимает объект класса `BankAccount`) и, возможно, «хвоста» (косвенно доступная память), то можно сказать, что синхронизированный класс в состоянии защитить лишь «голову» данных, в то время как «хвост» остается разделяемым (`shared`). По этой причине типизация полей синхронизированного (`synchronized`) класса внутри метода выполняется особым образом: -- значения любых числовых типов не разделяются (у них нет хвоста), -так что с ними можно обращаться как обычно (не применяется атри -бут `shared`); -- поля-массивы, тип которых объявлен как `T[]`, получают тип `shared(T)[]`; то есть голова (границы среза) не разделяется, а хвост (со -держимое массива) остается разделяемым; -- поля-указатели, тип которых объявлен как `T*`, получают тип `shared(T)*`; то есть голова (сам указатель) не разделяется, а хвост (дан -ные, на которые указывает указатель) остается разделяемым; -- поля-классы, тип которых объявлен как `T`, получают тип `shared(T)`. -К классам можно обратиться лишь по ссылке (это делается автома -тически), так что они представляют собой «сплошной хвост». +- значения любых числовых типов не разделяются (у них нет хвоста), так что с ними можно обращаться как обычно (не применяется атрибут `shared`); +- поля-массивы, тип которых объявлен как `T[]`, получают тип `shared(T)[]`; то есть голова (границы среза) не разделяется, а хвост (содержимое массива) остается разделяемым; +- поля-указатели, тип которых объявлен как `T*`, получают тип `shared(T)*`; то есть голова (сам указатель) не разделяется, а хвост (данные, на которые указывает указатель) остается разделяемым; +- поля-классы, тип которых объявлен как `T`, получают тип `shared(T)`. К классам можно обратиться лишь по ссылке (это делается автоматически), так что они представляют собой «сплошной хвост». -Эти правила накладываются поверх правила о запрете «утечек», опи -санного в предыдущем разделе. Прямое следствие такой совокупности -правил: операции, затрагивающие непосредственные поля объекта, -внутри метода можно свободно переупорядочивать и оптимизировать, -как если бы разделение этих полей было временно остановлено – а имен -но это и делает `synchronized`. +Эти правила накладываются поверх правила о запрете «утечек», описанного в предыдущем разделе. Прямое следствие такой совокупности правил: операции, затрагивающие непосредственные поля объекта, внутри метода можно свободно переупорядочивать и оптимизировать, как если бы разделение этих полей было временно остановлено – а именно это и делает `synchronized`. -Иногда один объект полностью владеет другим. Предположим, что -класс `BankAccount` сохраняет все свои предыдущие транзакции в списке -значений типа `double`: +Иногда один объект полностью владеет другим. Предположим, что класс `BankAccount` сохраняет все свои предыдущие транзакции в списке значений типа `double`: ```d // Не синхронизируется и вообще понятия не имеет о потоках @@ -1910,77 +861,26 @@ synchronized class BankAccount } ``` -Класс `List` не проектировался специально для разделения между пото -ками, поэтому он не использует никакой механизм синхронизации, но -к нему и на самом деле никогда не обращаются параллельно! Все обра -щения к этому классу замурованы в объекте класса `BankAccount` и полно -стью защищены, поскольку находятся внутри синхронизированных ме -тодов. Если предполагать, что `List` не станет затевать никаких безумных -проделок вроде сохранения некоторого внутреннего указателя в гло -бальной переменной, такой код должен быть вполне приемлемым. +Класс `List` не проектировался специально для разделения между потоками, поэтому он не использует никакой механизм синхронизации, но к нему и на самом деле никогда не обращаются параллельно! Все обращения к этому классу замурованы в объекте класса `BankAccount` и полностью защищены, поскольку находятся внутри синхронизированных методов. Если предполагать, что `List` не станет затевать никаких безумных проделок вроде сохранения некоторого внутреннего указателя в глобальной переменной, такой код должен быть вполне приемлемым. -К сожалению, это не так. В языке D код, подобный приведенному выше, -не заработает никогда, поскольку вызов метода `append` применительно -к объекту типа `shared(List!double)` некорректен. Одна из очевидных при -чин отказа компилятора от такого кода в том, что компиляторы никому -не верят на слово. Класс `List` может хорошо себя вести и все такое, но -компилятору потребуется более веское доказательство того, что за его -спиной не происходит никакое создание псевдонимов разделяемых дан -ных. В теории компилятор мог бы пойти дальше и проверить определе -ние класса `List`, однако `List`, в свою очередь, мог бы использовать другие -компоненты, расположенные в других модулях, так что не успеете вы -сказать «межпроцедурный анализ», как код «на обещаниях» начнет -выходить из-под контроля. +К сожалению, это не так. В языке D код, подобный приведенному выше, не заработает никогда, поскольку вызов метода `append` применительно к объекту типа `shared(List!double)` некорректен. Одна из очевидных причин отказа компилятора от такого кода в том, что компиляторы никому не верят на слово. Класс `List` может хорошо себя вести и все такое, но компилятору потребуется более веское доказательство того, что за его спиной не происходит никакое создание псевдонимов разделяемых данных. В теории компилятор мог бы пойти дальше и проверить определение класса `List`, однако `List`, в свою очередь, мог бы использовать другие компоненты, расположенные в других модулях, так что не успеете высказать «межпроцедурный анализ», как код «на обещаниях» начнет выходить из-под контроля. -*Межпроцедурный анализ* – это техника, применяемая компиляторами -и анализаторами программ для доказательства справедливости предпо -ложений о программе с помощью одновременного рассмотрения сразу -нескольких функций. Такие алгоритмы анализа обычно обладают низ -кой скоростью, начинают хуже работать с ростом программы и являют -ся заклятыми врагами раздельной компиляции. Некоторые системы -используют межпроцедурный анализ, но большинство современных -языков (включая D) выполняют все проверки типов, не прибегая к этой -технике. +*Межпроцедурный анализ* – это техника, применяемая компиляторами и анализаторами программ для доказательства справедливости предположений о программе с помощью одновременного рассмотрения сразу нескольких функций. Такие алгоритмы анализа обычно обладают низкой скоростью, начинают хуже работать с ростом программы и являются заклятыми врагами раздельной компиляции. Некоторые системы используют межпроцедурный анализ, но большинство современных языков (включая D) выполняют все проверки типов, не прибегая к этой технике. -Альтернативное решение проблемы с подобъектом-собственностью – -ввести новые квалификаторы, которые бы описывали отношения владе -ния, такие как «класс `BankAccount` является владельцем своего внутрен -него элемента `_transactions`, следовательно, мьютекс `BankAccount` также -обеспечивает последовательное выполнение операций над `_transactions`». При верном расположении таких примечаний компилятор смог бы -получить подтверждение того, что объект `_transactions` полностью ин -капсулирован внутри `BankAccount`, а потому безопасен в использовании, -и к нему можно обращаться, не беспокоясь о неуместном разделении. -Системы и языки, работающие по такому принципу, уже -были представлены, однако в настоящий момент они погоды не делают. -Ввод явного указания имеющихся отношений владения свидетельству -ет о появлении в языке и компиляторе значительных сложностей. Учи -тывая, что в настоящее время синхронизация на основе блокировок -борется за существование, D поостерегся усиливать поддержку этой -ущербной техники программирования. Не исключено, что это решение -еще будет пересмотрено (для D были предложены системы моделирова -ния отношений владения), но на настоящий момент, чтобы реализо -вать некоторые проектные решения, основанные на блокировках, при -ходится, как объясняется далее, переступать границы системы типов. +Альтернативное решение проблемы с подобъектом-собственностью – ввести новые квалификаторы, которые бы описывали отношения владения, такие как «класс `BankAccount` является владельцем своего внутреннего элемента `_transactions`, следовательно, мьютекс `BankAccount` также обеспечивает последовательное выполнение операций над `_transactions`». При верном расположении таких примечаний компилятор смог бы получить подтверждение того, что объект `_transactions` полностью инкапсулирован внутри `BankAccount`, а потому безопасен в использовании, и к нему можно обращаться, не беспокоясь о неуместном разделении. Системы и языки, работающие по такому принципу, уже были представлены, однако в настоящий момент они погоды не делают. Ввод явного указания имеющихся отношений владения свидетельствует о появлении в языке и компиляторе значительных сложностей. Учитывая, что в настоящее время синхронизация на основе блокировок борется за существование, D поостерегся усиливать поддержку этой ущербной техники программирования. Не исключено, что это решение еще будет пересмотрено (для D были предложены системы моделирования отношений владения), но на настоящий момент, чтобы реализовать некоторые проектные решения, основанные на блокировках, приходится, как объясняется далее, переступать границы системы типов. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-14-2-локальная-защита-разделение-хвостов) [Наверх ⮍](#13-параллельные-вычисления) ### 13.14.3. Принудительные идентичные мьютексы -D позволяет сделать динамически то, что система типов не в состоянии -гарантировать статически: отношение «владелец/собственность» в кон -тексте блокирования. Для этого предлагается следующая глобально -доступная базовая функция: +D позволяет сделать динамически то, что система типов не в состоянии гарантировать статически: отношение «владелец/собственность» в контексте блокирования. Для этого предлагается следующая глобально доступная базовая функция: ```d // Внутри object.d setSameMutex(shared Object ownee, shared Object owner); ``` -Объект `obj` некоторого класса может сделать вызов `obj.setSameMutex(owner)`[^12], и в результате вместо текущего объекта синхронизации `obj` нач -нет использовать тот же объект синхронизации, что и объект `owner`. Та -ким способом можно гарантировать, что при блокировке объекта `owner` -блокируется и объект `obj`. Посмотрим, как это сработает применитель -но к нашим подопытным классам `BankAccount` и `List`. +Объект `obj` некоторого класса может сделать вызов `obj.setSameMutex(owner)`[^12], и в результате вместо текущего объекта синхронизации `obj` начнет использовать тот же объект синхронизации, что и объект `owner`. Таким способом можно гарантировать, что при блокировке объекта `owner` блокируется и объект `obj`. Посмотрим, как это сработает применительно к нашим подопытным классам `BankAccount` и `List`. ```d // В курсе о существовании потоков @@ -2008,31 +908,13 @@ synchronized class BankAccount } ``` -Необходимое условие работы такой схемы – синхронизация обращений -к `List` (объекту-собственности). Если бы к объекту `_transactions` приме -нялись лишь обычные правила для полей, впоследствии при выполне -нии над ним операций он бы просто заблокировался в соответствии -с этими правилами. Но на самом деле при обращении к `_transactions` -происходит кое-что необычное: осуществляется явный захват мьютек -са объекта типа `BankAccount`. При такой схеме мы получаем довольный -компилятор: он думает, что каждый объект блокируется по отдельно -сти. Довольна и программа: на самом деле единственный мьютекс кон -тролирует как объект типа `BankAccount`, так и подобъект типа List. За -хват мьютекса поля `_transactions` – это в действительности захват уже -заблокированного мьютекса объекта `this`. К счастью, такой рекурсив -ный захват уже заблокированного, не запрашиваемого другими пото -ками мьютекса обходится относительно дешево, так что представлен -ный в примере код корректен и не снижает производительность про -граммы за счет частого блокирования. +Необходимое условие работы такой схемы – синхронизация обращений к `List` (объекту-собственности). Если бы к объекту `_transactions` применялись лишь обычные правила для полей, впоследствии при выполнении над ним операций он бы просто заблокировался в соответствии с этими правилами. Но на самом деле при обращении к `_transactions` происходит кое-что необычное: осуществляется явный захват мьютекса объекта типа `BankAccount`. При такой схеме мы получаем довольный компилятор: он думает, что каждый объект блокируется по отдельности. Довольна и программа: на самом деле единственный мьютекс контролирует как объект типа `BankAccount`, так и подобъект типа List. Захват мьютекса поля `_transactions` – это в действительности захват уже заблокированного мьютекса объекта `this`. К счастью, такой рекурсивный захват уже заблокированного, не запрашиваемого другими потоками мьютекса обходится относительно дешево, так что представленный в примере код корректен и не снижает производительность программы за счет частого блокирования. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-14-3-принудительные-идентичные-мьютексы) [Наверх ⮍](#13-параллельные-вычисления) ### 13.14.4. Фильм ужасов: приведение от shared -Продолжим работать с предыдущим примером. Если вы абсолютно уве -рены в том, что желаете возвести список `_transactions` в ранг святой част -ной собственности объекта типа `BankAccount`, то можете избавиться от -`shared` и использовать `_transactions` без учета потоков: +Продолжим работать с предыдущим примером. Если вы абсолютно уверены в том, что желаете возвести список `_transactions` в ранг святой частной собственности объекта типа `BankAccount`, то можете избавиться от `shared` и использовать `_transactions` без учета потоков: ```d // Не синхронизируется и вообще понятия не имеет о потоках @@ -2070,27 +952,13 @@ synchronized class BankAccount } ``` -На этот раз код с несинхронизированным классом `List` и компилирует -ся, и запускается. Однако есть одно «но»: теперь корректность основан -ной на блокировках дисциплины в программе гарантируете вы, а не -система типов языка, так что ваше положение не намного лучше, чем -при использовании языков с разделением данных по умолчанию. Пре -имущество, которым вы все же можете наслаждаться, состоит в том, -что приведения типов локализованы, а значит, их легко находить, то -есть тщательно рассмотреть обращения к `cast` на предмет ошибок – не -проблема. +На этот раз код с несинхронизированным классом `List` и компилируется, и запускается. Однако есть одно «но»: теперь корректность основанной на блокировках дисциплины в программе гарантируете вы, а не система типов языка, так что ваше положение не намного лучше, чем при использовании языков с разделением данных по умолчанию. Преимущество, которым вы все же можете наслаждаться, состоит в том, что приведения типов локализованы, а значит, их легко находить, то есть тщательно рассмотреть обращения к `cast` на предмет ошибок – не проблема. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-14-4-фильм-ужасов-приведение-от-shared) [Наверх ⮍](#13-параллельные-вычисления) ## 13.15. Взаимоблокировки и инструкция synchronized -Если пример с банковским счетом – это «Здравствуй, мир!» программ, -использующих потоки, то пример с переводом средств со счета на счет, -надо полагать, – соответствующее (но более мрачное) введение в пробле -му межпоточных взаимоблокировок. Условия для задачи с переводом -средств формулируются так: пусть даны два объекта типа `BankAccount` -(скажем, `checking` и `savings`); требуется определить атомарный перевод -некоторого количества денежных средств с одного счета на другой. +Если пример с банковским счетом – это «Здравствуй, мир!» программ, использующих потоки, то пример с переводом средств со счета на счет, надо полагать, – соответствующее (но более мрачное) введение в проблему межпоточных взаимоблокировок. Условия для задачи с переводом средств формулируются так: пусть даны два объекта типа `BankAccount` (скажем, `checking` и `savings`); требуется определить атомарный перевод некоторого количества денежных средств с одного счета на другой. Типичное наивное решение выглядит так: @@ -2103,14 +971,9 @@ void transfer(shared BankAccount source, shared BankAccount target, double amoun } ``` -Тем не менее эта версия не атомарна; в промежутке между двумя вызо -вами в теле `transfer` деньги отсутствуют на обоих счетах. Если точно -в этот момент времени другой поток выполнит функцию `inspectForAuditing`, обстановка может обостриться. +Тем не менее эта версия не атомарна; в промежутке между двумя вызовами в теле `transfer` деньги отсутствуют на обоих счетах. Если точно в этот момент времени другой поток выполнит функцию `inspectForAuditing`, обстановка может обостриться. -Чтобы сделать операцию перевода средств атомарной, потребуется осу -ществить захват скрытых мьютексов двух объектов за пределами их -методов, в начале функции `transfer`. Это можно организовать с помо -щью инструкций `synchronized`: +Чтобы сделать операцию перевода средств атомарной, потребуется осуществить захват скрытых мьютексов двух объектов за пределами их методов, в начале функции `transfer`. Это можно организовать с помощью инструкций `synchronized`: ```d // Перевод средств. Версия 2: ГЕНЕРАТОР ПРОБЛЕМ @@ -2127,23 +990,11 @@ void transfer(shared BankAccount source, shared BankAccount target, double amoun } ``` -Инструкция `synchronized` захватывает скрытый мьютекс объекта на -время выполнения своего тела. За счет этого вызванные методы данно -го объекта имеют преимущество уже установленного блока. +Инструкция `synchronized` захватывает скрытый мьютекс объекта на время выполнения своего тела. За счет этого вызванные методы данного объекта имеют преимущество уже установленного блока. -Проблема со второй версией функции `transfer` в том, что она предраспо -ложена к взаимоблокировкам (тупикам): если два потока попытаются -выполнить операции перевода между одними и теми же счетами, но -*в противоположных направлениях*, то эти потоки могут заблокировать -друг друга навсегда. Поток, пытающийся перевести деньги со счета `checking` на счет `savings`, блокирует счет `checking`, а другой поток, пытаю -щийся перевести деньги со счета `savings` на счет `checking`, совершенно -симметрично умудряется заблокировать счет `savings`. Этот момент ха -рактеризуется тем, что каждый из потоков удерживает свой мьютекс, -но для продолжения работы каждому из потоков необходим мьютекс -другого потока. К согласию такие потоки не придут никогда. +Проблема со второй версией функции `transfer` в том, что она предрасположена к взаимоблокировкам (тупикам): если два потока попытаются выполнить операции перевода между одними и теми же счетами, но *в противоположных направлениях*, то эти потоки могут заблокировать друг друга навсегда. Поток, пытающийся перевести деньги со счета `checking` на счет `savings`, блокирует счет `checking`, а другой поток, пытающийся перевести деньги со счета `savings` на счет `checking`, совершенно симметрично умудряется заблокировать счет `savings`. Этот момент характеризуется тем, что каждый из потоков удерживает свой мьютекс, но для продолжения работы каждому из потоков необходим мьютекс другого потока. К согласию такие потоки не придут никогда. -Решить эту проблему позволяет инструкция `synchronized` с *двумя* аргу -ментами: +Решить эту проблему позволяет инструкция `synchronized` с *двумя* аргументами: ```d // Перевод средств. Версия 3: верная @@ -2157,72 +1008,21 @@ void transfer(shared BankAccount source, shared BankAccount target, double amoun } ``` -Синхронизация обращений сразу к нескольким объектам с помощью -одной и той же инструкции `synchronized` и последовательная синхрони -зация каждого из этих объектов – разные вещи. Сгенерированный код -захватывает мьютексы всегда в том же порядке во всех потоках, невзи -рая на синтаксический порядок, в котором вы укажете объекты син -хронизации. Таким образом, взаимоблокировки предотвращаются. +Синхронизация обращений сразу к нескольким объектам с помощью одной и той же инструкции `synchronized` и последовательная синхронизация каждого из этих объектов – разные вещи. Сгенерированный код захватывает мьютексы всегда в том же порядке во всех потоках, невзирая на синтаксический порядок, в котором вы укажете объекты синхронизации. Таким образом, взаимоблокировки предотвращаются. -В случае эталонной реализации компилятора истинный порядок уста -новки блокировок соответствует порядку увеличения адресов объектов. -Но здесь подходит любой порядок, лишь бы он учитывал все объекты. +В случае эталонной реализации компилятора истинный порядок установки блокировок соответствует порядку увеличения адресов объектов. Но здесь подходит любой порядок, лишь бы он учитывал все объекты. -Инструкция `synchronized` с несколькими аргументами помогает, но, к со -жалению, не всегда. В общем случае действия, вызывающие взаимобло -кировку, могут быть «территориально распределены»: один мьютекс за -хватывается в одной функции, затем другой – в другой и так далее до -тех пор, пока круг не замкнется и не возникнет тупик. Однако `synchronized` со множеством аргументов дает дополнительные знания о пробле -ме и способствует написанию корректного кода с блочным захватом -мьютексов. +Инструкция `synchronized` с несколькими аргументами помогает, но, к сожалению, не всегда. В общем случае действия, вызывающие взаимоблокировку, могут быть «территориально распределены»: один мьютекс захватывается в одной функции, затем другой – в другой и так далее до тех пор, пока круг не замкнется и не возникнет тупик. Однако `synchronized` со множеством аргументов дает дополнительные знания о проблеме и способствует написанию корректного кода с блочным захватом мьютексов. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-15-взаимоблокировки-и-инструкция-synchronized) [Наверх ⮍](#13-параллельные-вычисления) ## 13.16. Кодирование без блокировок с помощью разделяемых классов -Теория синхронизации, основанной на блокировках, сформировалась -в 1960-х. Но уже к 1972 году исследователи стали искать пути ис -ключения из многопоточных программ медленных, неуклюжих мью -тексов, насколько это возможно. Например, операции присваивания -с некоторыми типами можно было выполнять атомарно, и программи -сты осознали, что охранять такие присваивания с помощью захвата -мьютексов нет нужды. Кроме того, некоторые процессоры стали выпол -нять транзакционно и более сложные операции, такие как атомарное -увеличение на единицу или «проверить-и-установить». Около тридцати -лет спустя, в 1990 году, появился луч надежды, который выглядел впол -не определенно: казалось, должна отыскаться какая-то хитрая комби -нация регистров для чтения и записи, позволяющая избежать тирании -блокировок. И в этот момент появилась полная плодотворных идей ра -бота, которая положила конец исследованиям в этом направлении, -предложив другое. +Теория синхронизации, основанной на блокировках, сформировалась в 1960-х. Но уже к 1972 году исследователи стали искать пути исключения из многопоточных программ медленных, неуклюжих мьютексов, насколько это возможно. Например, операции присваивания с некоторыми типами можно было выполнять атомарно, и программисты осознали, что охранять такие присваивания с помощью захвата мьютексов нет нужды. Кроме того, некоторые процессоры стали выполнять транзакционно и более сложные операции, такие как атомарное увеличение на единицу или «проверить-и-установить». Около тридцати лет спустя, в 1990 году, появился луч надежды, который выглядел вполне определенно: казалось, должна отыскаться какая-то хитрая комбинация регистров для чтения и записи, позволяющая избежать тирании блокировок. И в этот момент появилась полная плодотворных идей работа, которая положила конец исследованиям в этом направлении, предложив другое. -Статья Мориса Херлихи «Синхронизация без ожидания» (1991) озна -меновала мощный рывок в развитии параллельных вычислений. До это -го разработчикам и аппаратного, и программного обеспечения было оди -наково неясно, с какими примитивами синхронизации лучше всего ра -ботать. Например, процессор, который поддерживает атомарные опера -ции чтения и записи значений типа `int`, интуитивно могли счесть менее -мощным, чем тот, который помимо названных операций поддерживает -еще и атомарную операцию `+=`, а третий, который вдобавок предостав -ляет атомарную операцию `*=`, казался еще мощнее. В общем, чем боль -ше атомарных примитивов в распоряжении пользователя, тем лучше. +Статья Мориса Херлихи «Синхронизация без ожидания» (1991) ознаменовала мощный рывок в развитии параллельных вычислений. До этого разработчикам и аппаратного, и программного обеспечения было одинаково неясно, с какими примитивами синхронизации лучше всего работать. Например, процессор, который поддерживает атомарные операции чтения и записи значений типа `int`, интуитивно могли счесть менее мощным, чем тот, который помимо названных операций поддерживает еще и атомарную операцию `+=`, а третий, который вдобавок предоставляет атомарную операцию `*=`, казался еще мощнее. В общем, чем больше атомарных примитивов в распоряжении пользователя, тем лучше. -Херлихи разгромил эту теорию, в частности показав фактическую бес -полезность казавшихся мощными примитивов синхронизации, таких -как «проверить-и-установить», «получить-и-сложить» и даже глобаль -ная разделяемая очередь типа FIFO. В свете этих *парадоксов* мгновенно -развеялась иллюзия, что из подобных механизмов можно добыть маги -ческий эликсир для параллельных вычислений. К счастью, помимо по -лучения этих неутешительных результатов Херлихи доказал справед -ливость *выводов об универсальности*: определенные примитивы син -хронизации могут теоретически синхронизировать любое количество -параллельно выполняющихся потоков. Поразительно, но реализовать -«хорошие» примитивы ничуть не труднее, чем «плохие», причем на не -вооруженный глаз они не кажутся особенно мощными. Из всех полез -ных примитивов синхронизации прижился лишь один, известный как -сравнение с обменом (compare-and-swap). Сегодня этот примитив реали -зует фактически любой процессор. Семантика операции сравнения с об -меном: +Херлихи разгромил эту теорию, в частности показав фактическую бесполезность казавшихся мощными примитивов синхронизации, таких как «проверить-и-установить», «получить-и-сложить» и даже глобальная разделяемая очередь типа FIFO. В свете этих *парадоксов* мгновенно развеялась иллюзия, что из подобных механизмов можно добыть магический эликсир для параллельных вычислений. К счастью, помимо получения этих неутешительных результатов Херлихи доказал справедливость *выводов об универсальности*: определенные примитивы синхронизации могут теоретически синхронизировать любое количество параллельно выполняющихся потоков. Поразительно, но реализовать «хорошие» примитивы ничуть не труднее, чем «плохие», причем на невооруженный глаз они не кажутся особенно мощными. Из всех полезных примитивов синхронизации прижился лишь один, известный как сравнение с обменом (compare-and-swap). Сегодня этот примитив реализует фактически любой процессор. Семантика операции сравнения с обменом: ```d // Эта функция выполняется атомарно @@ -2237,41 +1037,15 @@ bool cas(T)(shared(T) * here, shared(T) ifThis, shared(T) writeThis) } ``` -В переводе на обычный язык операция `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`, любых массивов, любых -указателей и любых ссылок на классы. +В переводе на обычный язык операция `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-кодирование-без-блокировок-с-помощью-разделяемых-классов) [Наверх ⮍](#13-параллельные-вычисления) ### 13.16.1. Разделяемые классы -Херлиховские доказательства универсальности спровоцировали появ -ление и рост популярности множества структур данных и алгоритмов -в духе нарождающегося «программирования на основе `cas»`. Но есть -один нюанс: хотя реализация на основе `cas` и возможна теоретически для -любой задачи синхронизации, но никто не сказал, что это легко. Опреде -ление структур данных и алгоритмов на основе `cas` и особенно доказа -тельство корректности их работы – дело нелегкое. К счастью, однажды -определив и инкапсулировав такую сущность, ее можно повторно ис -пользовать для решения самых разных задач. +Херлиховские доказательства универсальности спровоцировали появление и рост популярности множества структур данных и алгоритмов в духе нарождающегося «программирования на основе `cas»`. Но есть один нюанс: хотя реализация на основе `cas` и возможна теоретически для любой задачи синхронизации, но никто не сказал, что это легко. Определение структур данных и алгоритмов на основе `cas` и особенно доказательство корректности их работы – дело нелегкое. К счастью, однажды определив и инкапсулировав такую сущность, ее можно повторно использовать для решения самых разных задач. -Чтобы ощутить благодать программирования без блокировок на основе -`cas`, воспользуйтесь атрибутом `shared` применительно к классу или -структуре: +Чтобы ощутить благодать программирования без блокировок на основе `cas`, воспользуйтесь атрибутом `shared` применительно к классу или структуре: ```d shared struct LockFreeStruct @@ -2285,23 +1059,13 @@ shared class LockFreeClass } ``` -Обычные правила относительно транзитивности в силе: разделяемость -распространяется на поля структуры или класса, а методы не предо -ставляют никакой особой защиты. Все, на что вы можете рассчиты -вать, – это атомарные присваивания, вызовы `cas`, уверенность в том, что -ни компилятор, ни машина не переупорядочат операции, и собственная -безграничная самоуверенность. Однако остерегайтесь: если написание -кода – ходьба, а передача сообщений – бег трусцой, то программирова -ние без блокировок – Олимпийские игры, не меньше. +Обычные правила относительно транзитивности в силе: разделяемость распространяется на поля структуры или класса, а методы не предоставляют никакой особой защиты. Все, на что вы можете рассчитывать, – это атомарные присваивания, вызовы `cas`, уверенность в том, что ни компилятор, ни машина не переупорядочат операции, и собственная безграничная самоуверенность. Однако остерегайтесь: если написание кода – ходьба, а передача сообщений – бег трусцой, то программирование без блокировок – Олимпийские игры, не меньше. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-16-1-разделяемые-классы) [Наверх ⮍](#13-параллельные-вычисления) ### 13.16.2. Пара структур без блокировок -Для разминки реализуем стек без блокировок. Основная идея проста: -стек моделируется с помощью односвязного списка, операции вставки -и удаления выполняются для элементов, расположенных в начале это -го списка: +Для разминки реализуем стек без блокировок. Основная идея проста: стек моделируется с помощью односвязного списка, операции вставки и удаления выполняются для элементов, расположенных в начале этого списка: ```d shared struct Stack(T) @@ -2343,61 +1107,17 @@ shared struct Stack(T) } ``` -`Stack` является разделяемой структурой, отсюда прямое следствие: внут -ри нее практически все тоже разделяется. Внутренний тип `Node` – клас -сическая структура «полезные данные + указатель», а сам тип `Stack` -хранит указатель на начало списка. +`Stack` является разделяемой структурой, отсюда прямое следствие: внутри нее практически все тоже разделяется. Внутренний тип `Node` – классическая структура «полезные данные + указатель», а сам тип `Stack` хранит указатель на начало списка. -Циклы `do`/`while` в теле обеих функций, реализующих базовые операции -над стеком, могут показаться странноватыми, но в них нет ничего осо -бенного; медленно, но верно они прокладывают глубокую борозду в ко -ре головного мозга каждого будущего эксперта в `cas`-программирова -нии. Функция `push` работает так: сначала создается новый узел, в кото -ром будет сохранено новое значение. Затем в цикле переменной `_root` -присваивается указатель на новый узел, но *только* если тем временем -никакой другой поток не изменил ее! Вполне возможно, что другой по -ток также выполнил какую-то операцию со стеком, так что функции -`push` нужно удостовериться в том, что указатель на начало стека, кото -рому, как предполагается, соответствует значение переменной `oldRoot`, -не изменился за время подготовки нового узла. +Циклы `do`/`while` в теле обеих функций, реализующих базовые операции над стеком, могут показаться странноватыми, но в них нет ничего особенного; медленно, но верно они прокладывают глубокую борозду в коре головного мозга каждого будущего эксперта в `cas`-программировании. Функция `push` работает так: сначала создается новый узел, в котором будет сохранено новое значение. Затем в цикле переменной `_root` присваивается указатель на новый узел, но *только* если тем временем никакой другой поток не изменил ее! Вполне возможно, что другой поток также выполнил какую-то операцию со стеком, так что функции `push` нужно удостовериться в том, что указатель на начало стека, которому, как предполагается, соответствует значение переменной `oldRoot`, не изменился за время подготовки нового узла. -Метод `pop` возвращает результат не по значению, а через указатель. При -чина в том, что `pop` может обнаружить очередь пустой, ведь это не явля -ется нештатной ситуацией (как было бы, будь перед нами стек, предна -значенный лишь для последовательных вычислений). В случае разде -ляемого стека проверка наличия элемента, его удаление и возврат со -ставляют одну согласованную операцию. За исключением возвращения -результата, функция `pop` по реализации напоминает функцию `push`: за -мена `_root` выполняется с большой осторожностью, так чтобы никакой -другой поток не изменил значение этой переменной, пока извлекаются -полезные данные. В конце цикла извлеченное значение отсутствует -в стеке и может быть спокойно возвращено инициатору вызова. +Метод `pop` возвращает результат не по значению, а через указатель. Причина в том, что `pop` может обнаружить очередь пустой, ведь это не является нештатной ситуацией (как было бы, будь перед нами стек, предназначенный лишь для последовательных вычислений). В случае разделяемого стека проверка наличия элемента, его удаление и возврат составляют одну согласованную операцию. За исключением возвращения результата, функция `pop` по реализации напоминает функцию `push`: замена `_root` выполняется с большой осторожностью, так чтобы никакой другой поток не изменил значение этой переменной, пока извлекаются полезные данные. В конце цикла извлеченное значение отсутствует в стеке и может быть спокойно возвращено инициатору вызова. -Если реализация класса `Stack` не показалась вам такой уж сложной, -возьмемся за реализацию более богатого односвязного интерфейса; -в конце концов большая часть инфраструктуры уже выстроена в рам -ках класса `Stack`. +Если реализация класса `Stack` не показалась вам такой уж сложной, возьмемся за реализацию более богатого односвязного интерфейса; в конце концов большая часть инфраструктуры уже выстроена в рамках класса `Stack`. -К сожалению, в случае со списком все угрожает быть гораздо сложнее. -Насколько сложнее? Нечеловечески сложнее. Одна из фундаменталь -ных проблем – вставка и удаление узлов в произвольных позициях спи -ска. Предположим, есть список значений типа `int`, а в нем есть узел -с числом `5`, за которым следует узел с числом `10`, и требуется удалить -узел с числом `5`. Тут проблем нет – просто пустите в ход волшебную опе -рацию cas, чтобы нацелить указатель `_root` на узел с числом `10`. Пробле -ма в том, что в то же самое время другой поток может вставлять новый -узел прямо после узла с числом `5` – узел, который будет безвозвратно -потерян, поскольку `_root` ничего не знает о нем. +К сожалению, в случае со списком все угрожает быть гораздо сложнее. Насколько сложнее? Нечеловечески сложнее. Одна из фундаментальных проблем – вставка и удаление узлов в произвольных позициях списка. Предположим, есть список значений типа `int`, а в нем есть узел с числом `5`, за которым следует узел с числом `10`, и требуется удалить узел с числом `5`. Тут проблем нет – просто пустите в ход волшебную операцию cas, чтобы нацелить указатель `_root` на узел с числом `10`. Проблема в том, что в то же самое время другой поток может вставлять новый узел прямо после узла с числом `5` – узел, который будет безвозвратно потерян, поскольку `_root` ничего не знает о нем. -В литературе представлено несколько возможных решений; ни одно из -них нельзя назвать тривиально простым. Реализация, представленная -ниже, впервые была предложена Тимоти Харрисом в его работе с мно -гообещающим названием «Прагматическая реализация неблокирую -щих односвязных списков». Эта реализация немного шероховата, -поскольку ее логика основана на установке младшего неиспользуемого -бита указателя `_next`. Идея состоит в том, чтобы сначала сделать на этом -указателе пометку «логически удален» (обнулив его бит), а затем на вто -ром шаге вырезать соответствующий узел целиком. +В литературе представлено несколько возможных решений; ни одно из них нельзя назвать тривиально простым. Реализация, представленная ниже, впервые была предложена Тимоти Харрисом в его работе с многообещающим названием «Прагматическая реализация неблокирующих односвязных списков». Эта реализация немного шероховата, поскольку ее логика основана на установке младшего неиспользуемого бита указателя `_next`. Идея состоит в том, чтобы сначала сделать на этом указателе пометку «логически удален» (обнулив его бит), а затем на втором шаге вырезать соответствующий узел целиком. ```d shared struct SharedList(T) @@ -2471,16 +1191,9 @@ shared struct SharedList(T) } ``` -Реализация непростая, но ее можно понять, если, разбирая код, дер -жать в голове пару инвариантов. Во-первых, для логически удаленных -узлов (то есть объектов типа `Node` с полем `_next`, младший бит которого -сброшен) вполне нормально повисеть какое-то время среди обычных уз -лов. Во-вторых, узел никогда не вставляют после удаленного узла. Та -ким образом, состояние списка остается корректным, несмотря на то, -что узлы могут появляться и исчезать в любой момент времени. +Реализация непростая, но ее можно понять, если, разбирая код, держать в голове пару инвариантов. Во-первых, для логически удаленных узлов (то есть объектов типа `Node` с полем `_next`, младший бит которого сброшен) вполне нормально повисеть какое-то время среди обычных узлов. Во-вторых, узел никогда не вставляют после удаленного узла. Таким образом, состояние списка остается корректным, несмотря на то, что узлы могут появляться и исчезать в любой момент времени. -Реализации функций `clearlsb`, `setlsb` и `haslsb` грубы, насколько это воз -можно; например: +Реализации функций `clearlsb`, `setlsb` и `haslsb` грубы, насколько это возможно; например: ```d T* setlsb(T)(T* p) @@ -2489,13 +1202,11 @@ T* setlsb(T)(T* p) } ``` -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-16-2-пара-структур-без-блокировок) [Наверх ⮍](#13-параллельные-вычисления) ## 13.17. Статические конструкторы и потоки[^13] -В одной из предыдущих глав была описана конструкция `static this()`, -предназначенная для инициализации статических данных модулей -и классов: +В одной из предыдущих глав была описана конструкция `static this()`, предназначенная для инициализации статических данных модулей и классов: ```d module counters;int counter = 0; @@ -2505,9 +1216,7 @@ static this() } ``` -Как уже говорилось, у каждого потока есть локальная копия перемен -ной `counter`. Каждый новый поток получает копию этой переменной, -и при создании этого потока запускается статический конструктор. +Как уже говорилось, у каждого потока есть локальная копия переменной `counter`. Каждый новый поток получает копию этой переменной, и при создании этого потока запускается статический конструктор. ```d import std.concurrency, std.stdio; @@ -2541,10 +1250,7 @@ void fun() Дочерний поток ``` -Объявить статический конструктор, исполняемый один раз при запус -ке программы и предназначенный для инициализации разделяемых -данных, можно с помощью конструкции `shared static this()`, а объя -вить разделяемый деструктор – с помощью конструкции `shared static ~this()`: +Объявить статический конструктор, исполняемый один раз при запуске программы и предназначенный для инициализации разделяемых данных, можно с помощью конструкции `shared static this()`, а объявить разделяемый деструктор – с помощью конструкции `shared static ~this()`: ```d import std.concurrency, std.stdio; @@ -2577,48 +1283,23 @@ void fun() Дочерний поток ``` -Разделяемыми могут быть не только конструкторы и деструкторы мо -дуля, но и статические конструкторы и деструкторы класса. Порядок -выполнения разделяемых статических конструкторов и деструкторов -определяется теми же правилами, что и порядок выполнения локаль -ных статических конструкторов и деструкторов. +Разделяемыми могут быть не только конструкторы и деструкторы модуля, но и статические конструкторы и деструкторы класса. Порядок выполнения разделяемых статических конструкторов и деструкторов определяется теми же правилами, что и порядок выполнения локальных статических конструкторов и деструкторов. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-17-статические-конструкторы-и-потоки-13) [Наверх ⮍](#13-параллельные-вычисления) ## 13.18. Итоги -Реализация функции `setlsb`, грязная и с потеками масла на стыках, -была бы подходящим заключением для главы, которая началась со -строгой красоты обмена сообщениями и постепенно спустилась в под -земный мир разделения данных. +Реализация функции `setlsb`, грязная и с потеками масла на стыках, была бы подходящим заключением для главы, которая началась со строгой красоты обмена сообщениями и постепенно спустилась в подземный мир разделения данных. -D предлагает широкий спектр средств для работы с потоками. Наибо -лее предпочтительный механизм для большинства приложений на со -временных машинах – определение протоколов на основе обмена сооб -щениями. При таком выборе может здорово пригодиться неизменяемое -разделение. Отличный совет для тех, кто хочет проектировать надеж -ные, масштабируемые приложения, использующие параллельные вы -числения, – организовать взаимодействие между потоками по методу -обмена сообщениями. +D предлагает широкий спектр средств для работы с потоками. Наиболее предпочтительный механизм для большинства приложений на современных машинах – определение протоколов на основе обмена сообщениями. При таком выборе может здорово пригодиться неизменяемое разделение. Отличный совет для тех, кто хочет проектировать надежные, масштабируемые приложения, использующие параллельные вычисления, – организовать взаимодействие между потоками по методу обмена сообщениями. -Если требуется определить синхронизацию на основе взаимоисключе -ния, это можно осуществить с помощью синхронизированных классов. -Но предупреждаю: по сравнению с другими языками, поддержка про -граммирования на основе блокировок в D ограничена, и на это есть ос -нования. +Если требуется определить синхронизацию на основе взаимоисключения, это можно осуществить с помощью синхронизированных классов. Но предупреждаю: по сравнению с другими языками, поддержка программирования на основе блокировок в D ограничена, и на это есть основания. -Если требуется простое разделение данных, можно воспользоваться раз -деляемыми (`shared`) значениями. D гарантирует, что операции с разде -ляемыми значениями выполняются в порядке, определенном в вашем -коде, и не провоцируют парадоксы видимости и низкоуровневые гонки. +Если требуется простое разделение данных, можно воспользоваться разделяемыми (`shared`) значениями. D гарантирует, что операции с разделяемыми значениями выполняются в порядке, определенном в вашем коде, и не провоцируют парадоксы видимости и низкоуровневые гонки. -Наконец, если вам наскучили такие аттракционы, как банджи-джам -пинг, укрощение крокодилов и прогулки по раскаленным углям, вы бу -дете счастливы узнать, что существует программирование без блокиро -вок и что вы можете заниматься этим в D, используя разделяемые -структуры и классы. +Наконец, если вам наскучили такие аттракционы, как банджи-джампинг, укрощение крокодилов и прогулки по раскаленным углям, вы будете счастливы узнать, что существует программирование без блокировок и что вы можете заниматься этим в D, используя разделяемые структуры и классы. -[В начало ⮍]() [Наверх ⮍](#13-параллельные-вычисления) +[В начало ⮍](#13-18-итоги) [Наверх ⮍](#13-параллельные-вычисления) [^1]: Число транзисторов на кристалл будет увеличиваться вдвое каждые 24 месяца. – *Прим. пер.* [^2]: Далее речь идет о параллельных вычислениях в целом и не рассматриваются распараллеливание операций над векторами и другие специализированные параллельные функции ядра.