Какое ограничение действует на параметр inout у async-функции и чем оно обосновано?
Асинхронная функция не может иметь параметр inout. Причина в том, что доступ к переданной переменной должен оставаться эксклюзивным на всём протяжении вызова, включая период после await, когда функция приостановлена.
Если разрешить такой параметр, другой код мог бы изменить ту же переменную во время приостановки. После возобновления функция продолжила бы работу с состоянием, которое изменилось независимо от неё, нарушив модель эксклюзивного доступа и безопасность данных.
inout появился как механизм безопасной передачи изменяемого значения с временным эксклюзивным доступом: вызываемая функция может читать и менять исходную переменную, а затем вернуть обновлённое значение владельцу.
Модель async/await добавила возможность приостанавливать функцию на неопределённое время. Для синхронного inout эксклюзивный доступ обычно ограничен длительностью вызова, но в асинхронном коде вызов может уступить выполнение другим задачам. Поэтому прежняя модель доступа несовместима с приостановкой.
Рассмотрим абстрактную операцию, которая получает переменную по inout, читает её, приостанавливается, а затем записывает результат. Пока операция ожидает внешний ресурс, другая задача или другой участок кода может получить доступ к той же переменной.
Это создаёт несколько рисков: одновременную запись, потерю обновления и использование устаревшего значения. Даже если фактический конфликт происходит редко, компилятор не может безопасно разрешить такой доступ без дополнительной синхронизации.
inout основан на эксклюзивном доступе: пока функция владеет доступом к переменной, несовместимый доступ к ней запрещён. Для async-функции этого недостаточно, потому что await разделяет выполнение на этапы, между которыми могут выполняться другие задачи.
Поэтому асинхронная операция должна принимать значение обычным параметром и возвращать новое значение:
Здесь переменная не удерживается эксклюзивно во время await. Функция получает снимок значения, а присваивание выполняется вызывающим кодом после завершения операции.
Если значение общее для нескольких задач, одного возврата результата недостаточно: две задачи могут прочитать одинаковое старое состояние и затем перезаписать результаты друг друга. Для такого случая состояние размещают внутри actor, используют Mutex для синхронной критической секции или выбирают другой явно синхронизированный дизайн.
Возврат значения не делает составную операцию атомарной автоматически. Он лишь устраняет недопустимое удержание inout через приостановку; атомарность общего обновления должна обеспечиваться отдельным механизмом синхронизации.
Сервис должен увеличить счётчик после асинхронного запроса. Вариант с передачей счётчика как inout в async-операцию невозможен: он пытался бы сохранить эксклюзивный доступ через потенциальный await.
Вариант с чтением счётчика, асинхронной обработкой и последующей записью без синхронизации прост, но небезопасен: две задачи могут одновременно прочитать значение 10 и обе записать 11. Вариант с глобальной блокировкой защищает данные, но усложняет владение блокировкой и опасен, если удерживать её через await.
Для состояния, естественно принадлежащего одному владельцу, выбран actor. Он выполняет изменение внутри изолированного метода без передачи inout наружу. Если операция должна быть строго синхронной и короткой, более подходящим решением будет Mutex. Такой выбор сохраняет атомарность и не требует удерживать блокировку во время асинхронного ожидания.
inout в коде, который связан с async-функцией?Нет. Ограничение относится к асинхронной функции как к получателю inout-параметра. Синхронная функция может использовать inout, а async-код может вызвать её, если сам эксклюзивный доступ не пересекает асинхронную приостановку.
Например, короткое синхронное обновление можно вынести в отдельную функцию. Но это безопасно только для участка без await; добавление ожидания внутрь этой критической операции снова потребует другого дизайна.
inout на обычный параметр и возвращаемое значение для защиты общего состояния?Нет. Такая замена устраняет проблему эксклюзивного доступа через await, но не устраняет гонку между чтением и записью общего состояния.
Если несколько задач обновляют один счётчик, операции чтения, вычисления и записи должны выполняться атомарно относительно друг друга. Для этого подходят actor, Mutex или иной механизм синхронизации, соответствующий длительности операции.
inout?Технически блокировку можно удерживать, но это обычно плохой компромисс. Пока задача находится на await, поток не обязан быть занят, однако логическая блокировка остаётся захваченной; другая задача может ждать её длительное время или возникнет взаимная блокировка при обратном порядке ожиданий.
Безопаснее не удерживать синхронную блокировку через await. Нужно либо завершить критическую секцию до приостановки, либо передать владение состоянием actor, который допускает приостановки без удержания обычного мьютекса.