Страницы

четверг, 10 ноября 2022 г.

Внедрение параллельности в однопоточный язык, не нарушающее совместимости

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

Основа

Исходные свойства должны оставаться неизменным для данных и действий, объявленных в соответствии с исходной однопоточной спецификацией языка, а нововведения в ожидаемых эффектах должны предоставляться только через нововведения же в языке. В том числе это означает, что для потока по-прежнему верно, что обычные переменные не могут измениться как-то иначе, кроме как в самом этом потоке. И тонкая грань, пролегающая между ломающим внедрением параллельности и совместимым, заключается почти исключительно в трактовке понятия глобальности. В первом случае она считается абсолютной, а во втором — относительной. Последнее означает, что потоки преимущественно логически изолируются друг от друга и большей частью своего состояния распоряжаются единолично, что гарантируется языком, а не просто дисциплиной разработчика. Важно отметить, что изоляция хороша не только для параллельности.

  • Обычные глобальные переменные должны быть глобальными лишь в рамках потока. Для системы в целом они должны быть локально привязаны к своим потокам.
  • Динамически выделямые данные тоже изначально логически обособлены и привязаны к потоку. Язык не должен позволять потокам просто так обмениваться обычными указателями.

Такой подход можно назвать многооднопоточностью. В ней запущенный параллельно код, даже если он никогда для этого не предназначался, не вступит в противоречие с другим потоком и будет выполняться ровно так же, как в однопоточной среде. В худшем случае это приведёт лишь к дополнительному расходу памяти из-за избыточного дублирования. Но не жертвуя корректностью систему можно постепенно дорабатывать, объединяя данные для которых это уместно. Работу с данными, общими по своей природе, в любом случае необходимо доводить относительно однопоточного кода.

Добавление

Обмен данными между потоками возможен через:

  • Значения, к которым можно причислить и указатели на неизменяемые данные. Не скрытые для изменения извне, а именно закреплённые. Такие данные не обязаны одномоментно создаваться такими, а могут проходить этап инициализации, в течении которого могут рассматриваться как изменяемые.
  • Изменяемые данные с передачей владения.
    • Передача результата при завершении подзадачи. Незадействованные в возврате данные могут быть эффективно освобождены локальной для потока сборкой мусора без необходимости обхода глобальной кучи и с минимальной пометкой полезных данных. Часть задач может быть гармонично разбита на очевидные этапы, для которых конец одного инициирует начало следующего. Разбиение на подзадачи полезно даже в однопоточности, и может рассматриваться как дополнительное средство структурирования.
    • Данные, уникальность владения которыми обеспечивается статически подтверждаемой концепцией владения. Надёжно, но может оказаться слишком ограниченным или слишком сложным для простого языка, как для воплощения транслятора, так и для использования программистом.
    • Данные, отсутствие лишних ссылок на которые подтверждается сборщиком мусора, удостоверяющего, что для исходного потока они стали лишними. Просто для программиста при оптимистическом сценарии, но может быть ресурсоёмким и возможны сложно отслеживаемые ошибки в коде, блокирующие передачу. Если для управления памятью используется автоматический подсчёт ссылок, то проверка на уникальность оказывается быстрой, но проблема блокировки передачи из-за забытых ссылок остаётся.
    • Хранилище, неразделяемо владеющее данными и предоставляющее доступ к ним только через своё посредничество. При передаче владения хранилище опустошается, гарантированно лишая доступа к данным со стороны первоначального владельца. Это эффективно и просто воплотимо, но требует введния дополнительго тип. Может приводить к ошибкам времени исполнения при неосторожных попытках обращения к данным из закрытого хранилища.
    • Нечто среднее между двумя предыдущими. Сборщик мусора не проверяет, а осуществляет отсутствие лишних ссылок на передаваемые данные, очищая ссылающиеся на них указатели. Может показаться удобным, но частично ломает совместимость, потому что создаёт возможность исчезновения данных, минуя обычные механизмы защиты данных.
  • Изменяемые данные с защитой от одновременного изменения. Либо явно обозначенные как разделяемые, либо скрытые за другими высокоуровневыми механизмами, например, каналами ввода-вывода.
    • Доступ к явному управлению блокировками стоит признать нежелательным, но может быть предоставлен как некий ограниченный низкоуровневый механизм.
    • Монопольный доступ на изменения может осуществляться через неявные блокировки.
    • Параллельное чтение допускается, но новые участники могут включаться в него только при отсутствии запроса на запись.
    • Для гарантии остутствия взаимных блокировок[0]
      • Необходимо ограничение доступа таким способом, чтобы процесс либо одновременно мог блокировать только один ресурс, либо несколько, но в строгом порядке. Может быть либо сковывающим для программиста, либо сложно воплотимым.
      • Либо динамическое отслеживание за количеством и порядком блокировок. Повышает гибкость и проще воплотимо, но способно приводить к ошибкам исполнения.
    • Потоки, обменивающиеся данными только через каналы ввода-вывода, являются частным случаем систем, в которых можно блокировать не более одного ресурса одновременно. Байтовые каналы и обёртки над ними — это решение для языков с минимальной поддержкой многопоточности. Более развитая поддержка подразумевает возможность обмена полноценными структурами, включая через указатели при соблюдении ранее описанных условий.
Примечания [0] Речь идёт о гарантиях отсутствия блокировок на уровне терминов языка. Понятие блокировки может быть воспроизведено в других терминах. Смотрите, например, код получения взаимных блокировок в однопоточном языке, запускаемом в многозадачной среде.
Смотрите также Рифат Сабирзянов - Concurrency with shared variable considered harmful на YouTube

Комментариев нет:

Отправить комментарий

Обработка ошибок

Тема корректной обработки ошибок в программе является довольно сложным вопросом в программировании. Отчасти от того, что и она сама являет...