Найти в Дзене
Unity и геймдев | aks2dio

Коварный AttachExternalCancellation

Если вы работаете с асинхронностью в Unity, то наверняка знакомы с UniTask. Это более эффективный и удобный для Unity инструмент, чем обычные шарповые Task. GitHub Его удобство часто достигается за счёт разнообразного "сахара", с которым важно не переборщить. Одна из таких "сладостей" — это метод AttachExternalCancellation, который очень часто используют не по назначению. —————————————— Обычно для прерывания асинхронной операции используют CancellationToken: его передают внутрь и отслеживают. Если во время исполнения был получен запрос на отмену, то нужно выбросить соответствующее исключение или прервать исполнение метода более мягко. 📎 Подробнее про это и полезные советы можно почитать здесь. Соответственно, если это какой-то метод UniTask, например Delay, то вся логика остановки внутри уже реализована — достаточно только передать токен. Почему-то принято считать, что AttachExternalCancellation работает таким же образом, хотя его можно приставить к любому асинхронному методу, который
Оглавление

Если вы работаете с асинхронностью в Unity, то наверняка знакомы с UniTask. Это более эффективный и удобный для Unity инструмент, чем обычные шарповые Task.

GitHub

Его удобство часто достигается за счёт разнообразного "сахара", с которым важно не переборщить. Одна из таких "сладостей" — это метод AttachExternalCancellation, который очень часто используют не по назначению.

——————————————

В чём проблема

Обычно для прерывания асинхронной операции используют CancellationToken: его передают внутрь и отслеживают.

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

📎 Подробнее про это и полезные советы можно почитать здесь.

Соответственно, если это какой-то метод UniTask, например Delay, то вся логика остановки внутри уже реализована — достаточно только передать токен.

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

-2

UniTask и Task — это не сопрограммы, которые привязаны к циклу конкретного MonoBehaviour. Соответственно, они не смогут остановиться магическим образом, если к ним просто "сбоку" приставить токен.

❗️ AttachExternalCancellation не останавливает выполнение кода внутри задачи. Он лишь останавливает ожидание этой задачи.

Т.е. если вы делали await какого-то метода — он прекратится, но сам асинхронный метод продолжит работать.

Похожим образом работает и метод ToUniTask(CancellationToken token). Ему тоже не стоит доверять.

Этот кейс, и некоторые другие, более подробно рассмотрены в этой статье на Хабр из числа моих любимых.

——————————————

Последствия

  • Непредсказуемое поведение: вы можете не ожидать, что якобы остановленный метод продолжит работать и как-то взаимодействовать с другими объектами, особенно от Unity, которые могут стать уничтоженными.
  • Лишняя нагрузка: метод продолжает работать и отъедать аппаратные ресурсы.
  • Утечки памяти: работающая задача держит ссылки на объекты и не отдаёт их сборщику мусора (об этом уже упоминалось в этом посте 💬)

——————————————

Когда это нужно

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

Например, загрузка Addressables. Но тут важно не просто прервать ожидание: нужно ещё в фоне дождаться завершения загрузки и выполнить затем Release. Иначе этот ассет останется невыгруженным.

-3

Т.е. некоторые такие "неотменяемые" операции обязательно нужно дожидаться и финализировать.

——————————————

Дополнительные рекомендации

  • Всегда передавайте CancellationToken в каждый асинхронный метод, в т.ч. вложенные.
  • Строго контролируйте жизненный цикл каждого такого метода и его CancellationTokenSource.
  • Не забывайте вызывать и Cancel, и Dispose для финализации CancellationTokenSource.
  • В MonoBehaviour можно использовать this.GetCancellationTokenOnDestroy(), привязанный к конкретному MonoBehaviour, по аналогии с Coroutine.