Давайте посмотрим на изменения параллелизма в Swift

С новыми изменениями, связанными с параллелизмом, которые появятся в Swift 5.5, язык делает еще проще для нас, разработчиков Swift, написание параллельного кода. Существует огромное количество предложений, которые были приняты в выпуске 5.5, и уже есть масса нового, что нам нужно изучить и к чему привыкнуть.

Одна из новых функций, которые появятся в новом выпуске Swift, - это доступность нового примитива под названием actor. Поэтому, прежде чем мы начнем использовать акторов, давайте попробуем понять, что они из себя представляют и какие изменения вносит Swift для поддержки этой конкретной модели «актеров» в языке.

Таким образом, статья будет разбита на два основных раздела. В первом разделе мы попытаемся понять, что такое участники, какова основная проблема, которую они пытаются решить, и как они ее решают. Затем мы рассмотрим, как Свифт представляет нам актеров.

Параллелизм и модель участников

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

Одна из самых больших проблем параллельных систем - это проблемы общего состояния. Чтобы быть более конкретным, управление общим состоянием часто приводит к двум типам проблем в параллельной системе:

  1. Гонка данных - когда два или более потока пытаются получить доступ (по крайней мере, один доступ является записью) к одному ресурсу, что вызывает несогласованность данных.
  2. Условия гонки - из-за недетерминированного выполнения фрагмента кода на общем ресурсе результат оказывается непредсказуемым в различных сценариях. В основном, поскольку порядок выполнения не детерминирован, он часто приводит к разным результатам.

Между ними есть разница, но, вообще говоря, они возникают потому, что есть какой-то доступ к общему состоянию. Кроме того, как правило, гораздо проще обнаружить гонку данных, но очень сложно отладить условия гонки, потому что первые могут быть воспроизведены, а вторые не могут воспроизводиться каждый раз.

Теперь существуют различные модели параллелизма, которые помогают нам решать проблемы гонки данных (например, блокировки и мьютексы, сериализованный доступ к общим данным и т. Д.).

Swift пытается избежать этой проблемы, поощряя нас использовать семантику значений (то есть структуры и перечисления), поскольку их, как правило, легче понять в параллельных средах.

Но даже семантика значений не помогает во всех случаях (либо потому, что они не имеют смысла для использования в этом контексте, либо из-за неправильной реализации), и в конечном итоге вы в конечном итоге используете какой-то механизм синхронизации, такой как блокировки или последовательные очереди. Здесь нам на помощь приходит новая модель Актера.

Акторная модель - это модель параллелизма, где actor - это новая примитивная структура, которая сохраняет и защищает локальное состояние, будучи единственной, которая может вносить в него изменения / мутации. Любой внешний член может просто попросить актера действовать в соответствии с его состоянием, и актор обеспечит синхронизацию всех запросов на доступ / изменение его состояния.

Актеры обладают следующими характеристиками:

  • Имеют собственное изолированное государство.
  • Может содержать логику для изменения собственного состояния.
  • Может общаться с другими участниками только асинхронно (через их адреса).
  • Может создавать других дочерних актеров (нас это не особо волнует).

Одно из лучших объяснений ELI5 для общения в модели Актера следующее:

«Представьте, что каждый актер похож на остров, а наша кодовая база - это мир с островами. Теперь каждый остров может общаться с другим островом, отправляя ему сообщения в бутылке. Каждый остров знает, куда отправить сообщение (то есть адрес другого острова), и именно так работает связь между каждым островом ».

Хотя теория, лежащая в основе модели Actor, содержит гораздо больше (я прикреплю различные ссылки внизу статьи), мы рассмотрим, как эта модель на самом деле транслируется в наш мир Swift.

Актеры в Swift

Swift 5.5 вводит новое ключевое слово actor. Так же, как вы можете определить class или struct, теперь вы можете определить actor.

Эти субъекты могут соответствовать протоколам и функционировать как любые из предыдущих примитивов (за исключением наследования, которое в настоящее время не поддерживается). Единственное отличие состоит в том, что взаимодействие между разными участниками происходит асинхронно.

Один из наиболее часто используемых примеров при объяснении параллелизма - это пример внесения / снятия денег с банковского счета. Итак, давайте продолжим и определим BankAccount актера:

В приведенном выше примере, если BankAccount был определен как class вместо actor, переменная balance могла считаться небезопасным «изменяемым состоянием» BankAccount, которое могло привести к потенциальным ситуациям гонки данных в параллельной среде. Но теперь, когда BankAccount был определен как actor, переменная balance защищена от скачков данных. Посмотрим как.

Актерская изоляция

Теперь защита от гонки данных, описанная выше, осуществляется с помощью концепции, называемой изоляцией акторов. Это просто термин, используемый для определения нескольких правил, касающихся того, как должен работать доступ между участниками (как функциями, так и свойствами). Правила следующие:

  1. Актер может читать свои собственные свойства или вызывать свои функции на себе (т. Е. Используя self) синхронно.
  2. Актер может обновлять только свои собственные свойства (и может делать это синхронно). Это означает, что вы можете обновлять свойства только с помощью ключевого слова self. Попытка обновить свойство другого актера приведет к ошибке компилятора.
  3. Считывание свойств между участниками или вызовы функций должны происходить асинхронно с использованием ключевого слова await. Однако перекрестное чтение неизменяемых свойств может происходить синхронно (те, которые объявлены с let).

Возвращение актера

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

Допустим, вы пытались закрыть свой банковский счет, и для этого вам необходимо связаться с серверами вашего банка. Мы предпринимаем следующие шаги:

  1. Проверяем, есть ли аккаунт isOpen или нет. Нет смысла закрывать уже закрытый аккаунт.
  2. Сообщите на серверы банка, что счет запрашивает закрытие (этот шаг может занять время).
  3. Проверьте, открыт ли еще счет. Если счет все еще открыт, закройте счет и верните остаток. В противном случае выдает ошибку, сообщающую, что во время сетевого вызова был выдан другой запрос на отмену, который, вероятно, уже закрыл учетную запись.

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

Например, в этом вызове close банковского счета вы можете «приостановить» свой код на некоторое время (пока он обменивается данными с серверами банка), заставить тот же поток выполнять некоторую другую работу, а затем «возобновить» выполнение работать с того места, где он остановился, как только вы получите ответ от серверов банка.

Предстоит небольшое, но важное обсуждение строки 8, в которой происходит «приостановка» работы текущего потока (т. Е. Строки, в которой происходит вызов await).

Помните, что каждый await вызов - это потенциальная точка приостановки вашего кода.

Пока серверы банка не ответят, этот поток может выполнять другую незавершенную работу, которую, вероятно, запланировал наш код, или какую-то новую работу, которую запрашивает наш код. Мы можем отправить вызов withdraw money, deposit money или другой запрос cancellation, и это в конечном итоге будет выполняться в этом потоке.

Теперь, когда сервер банка отвечает, состояние актера может отличаться от состояния до точки приостановки. Это очень важный момент, поскольку вы должны осознавать тот факт, что вы не можете делать предположения о состоянии вашего актера до и после вызова функции await.

Это единственная причина, по которой я повторно проверил, открыт ли счет еще или нет, в строке 9 (после вызова await), потому что вполне возможно, что второй вызов отмены мог быть выполнен и завершен, и учетная запись уже был закрыт.

Следовательно, рассуждая о повторном вхождении актера, вам нужно помнить две вещи:

  • Всегда пытайтесь выполнить изменение состояния в синхронном коде (избегайте async вызовов функций в функциях, в которых вы меняете внутреннее состояние).
  • Если вам нужно выполнить async вызовов функций внутри функции, которая изменяет состояние, не делайте никаких предположений об этом состоянии после завершения await.

@Главный актер

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

Swift 5.5 использует новую оболочку свойств, которая называется @MainActor. Эта аннотация гарантирует, что любой доступ для чтения и записи к свойствам, аннотированным с помощью этой оболочки, происходит в основном потоке (таким образом устраняются все вызовы DispatchQueue.main).

Вы можете аннотировать свойства, функции и определения классов / структур с помощью этой оболочки свойства.

Классы UIKit (например, UILabel, UIView и т. Д.) Уже отмечены этой оболочкой свойства. Таким образом, вы можете быть уверены, что они всегда будут доступны в основном потоке.

Единственная загвоздка в том, что эти члены будут доступны только в основном потоке при использовании новых вызовов async/await, а не при использовании обработчиков завершения. Вот фрагмент кода, который поможет вам лучше понять это:

Если вы запустите этот фрагмент кода и установите точку останова в строке 11/12, вы увидите, что вызов из DispatchQueue.global не будет выполняться в основном потоке, но вызов из asyncDetached будет выполняться в основном потоке.

Заключение

Как вы заметили, actors - определенно необходимое дополнение к такому современному языку, как Swift. Я уверен, что вся система будет продолжать развиваться, и в Swift может быть много обновлений параллелизма. С учетом сказанного, основные моменты, которые вам нужно вынести из этой статьи, следующие:

  • Акторы - это еще один метод, который можно использовать для решения проблемы гонки данных, которая возникает в параллельных системах.
  • Актеры используют концепцию изоляции акторов, чтобы предотвратить скачки данных.
  • Несмотря на то, что акторы помогают вам с гонками данных, все же есть точки разногласий, в которых могут возникнуть условия гонки. Следовательно, вам следует убедиться, что вы не делаете никаких предположений о состоянии актора всякий раз, когда вводите точки приостановки.
  • Использование @MainActor может помочь вам получить доступ к свойствам в основном потоке без DispatchQueue.main вызовов, а только с использованием новой async/await системы вызовов.

Ссылки