Ожидаемое завершение и продолжение
Оператор await приостанавливает выполнение окружающего асинхронного метода, что позволяет вызванному потоку выполнять другую работу во время ожидания. Когда ожидаемый Task или Awaitable завершается, асинхронный код должен возобновиться и продолжить выполнение с точки, где он был приостановлен. То, как возобновляется асинхронный код, может иметь важные последствия для функции и производительности вашего приложения.
.NET Продолжение работы
Сведения о том, в каком состоянии находился код, когда он начал ожидание, называются контекстом синхронизации. Платформа .NET предоставляет SynchronizationContext класс для сбора сведений такого рода. Task продолжения выполняются в контексте синхронизации, из которого был вызван асинхронный метод, или через поток если контекст синхронизации не установлен.
Большинство методов Unity APIs не являются безопасными для потоков и могут быть вызваны только из главного потока. По этой причине Unity перезаписывает стандартные SynchronizationContext с пользовательским UnitySynchronizationContext, чтобы обеспечить выполнение по умолчанию всех продолжений .NET Task как в режиме редактирования, так и в режиме воспроизведения на главном потоке. Если вы вызываете метод Task-returning из главного потока Unity, продолжение отправляется в UnitySynchronizationContext и выполняется на следующем кадре Update на главном потоке. Если вы вызываете его из фонового потока, он завершается на потоке пул-поток.
Захват контекста синхронизации увеличивает нагрузку на производительность приложения, а ожидание возобновления следующего кадра обновления на главном потоке вводит задержку в масштабе. Вы можете избежать обеих этих проблем, используя вместо этого Awaitable.
Ожидаемые продолжения
Если не указано иное, все экземпляры Awaitable, возвращаемые Unity APIs, а также любые определенные пользователем возвращаемые методы async Awaitable, имеют следующее поведение планирования продолжения:
- Если метод вызван из главного потока, он возобновляется на главном потоке.
- В противном случае она возобновляется на потоке .NET
ThreadPool.
Заметными исключениями являются:
Awaitable.MainThreadAsync: продолжение происходит на главном потоке.Awaitable.BackgroundThreadAsync: продолжение происходит на фоновом потоке.
Эффект Awaitable.MainThreadAsync и Awaitable.BackgroundThreadAsync локален только для текущего метода, например:
private async Awaitable<float> DoHeavyComputationInBackgroundAsync()
{
await Awaitable.BackgroundThreadAsync();
// here we are on a background thread
// do some heavy math here
return 42; // note: we don't need to explicitly get back to the main thread here, depending on the caller thread, DoHeavyComputationInBackgroundAsync will automatically complete on the correct one.
}
public async Awaitable Start()
{
var computationResult = await DoHeavyComputationInBackgroundAsync();
// although DoHeavyComputationInBackgroundAsync() internally switches to a background thread to avoid blocking,
// because we await it from the main thread, we also resume execution on the main thread and can safely call "main thread only APIs" such as LoadSceneAsync()
await SceneManager.LoadSceneAsync("my-scene"); // this will succeed as we resumed on main thread
}
Примечание: Unity не останавливает автоматически выполнение кода в фоновом режиме при выходе из режима воспроизведения. Для отмены фоновой операции при выходе из режима воспроизведения используйте Application.exitCancellationToken.
Переключение потоков и производительность
Наиболее эффективно вызывать await Awaitable.MainThreadAsync() из главного потока и await Awaitable.BackgroundThreadAsync() из фонового потока, так как в каждом случае код возобновляется сразу после завершения. Если вы переключитесь обратно на главный поток из фонового потока с помощью MainThreadAsync, ваш код не может возобновиться до следующего обновления кадра на главном потоке.
Если вы вызываете Task-returning API из главного потока и он не завершается синхронно, вам придется подождать, по крайней мере, до следующего Update (33 мс при 30 кадрах в секунду), чтобы продолжение было запущено. Если сетевой задержка является проблемой, рекомендуется делать это вне главного потока и использовать пользовательскую логику для синхронизации между главным потоком и сетевыми задачами.
В сборках для разработки Unity отображает следующее сообщение об ошибке, если вы пытаетесь использовать Unity APIs в многопоточном коде:
UnityException: Internal_CreateGameObject can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread when loading a scene.
Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.
Важно: По причинам производительности, Unity не проверяет многопоточное поведение в неразработочных сборках и не отображает эту ошибку в живых сборках. Хотя Unity не предотвращает выполнение многопоточного кода в этих контекстах, сбои и другие непредсказуемые ошибки являются вероятными, если вы используете несколько потоков. Вместо использования собственной многопоточности, безопаснее использовать систему заданий Unity. Система заданий использует несколько потоков для безопасного параллельного выполнения заданий и достижения преимуществ производительности многопоточности. Для получения дополнительной информации см. Обзор системы заданий.
Ожидаемое по сравнению с системой должностей
Unity Awaitable класс подходит лучше системы заданий в следующих случаях:
- Упрощение кода при выполнении по сути асинхронных операций, таких как манипулирование файлами или выполнение веб-запросов, без блокирования.
- Перегрузка долго работающих задач (>1 кадр) на фоновый поток.
- Модернизация корутин на основе итераторов.
- Ожидание нескольких видов асинхронных операций (события кадра, события Unity, сторонние асинхронные APIs, ввод/вывод).
Тем не менее, это не рекомендуется для операций с более коротким сроком действия, таких как параллелизация вычислительно-интенсивных алгоритмов. CPUs и параллелизировать ваши алгоритмы, используйте система работы вместо этого.
Триггер завершения из кода
AwaitableCompletionSource и AwaitableCompletionSource<T> позволяют создавать экземпляры Awaitable, где завершение инициируется пользовательским кодом. Например, так можно реализовать собственные подсказки, не строя машину состояний для ожидания завершения взаимодействия с пользователем:
public class UserNamePrompt : MonoBehaviour
{
TextField _userNameTextField;
AwaitableCompletionSource<string> _completionSource = new AwaitableCompletionSource<string>();
public void Start()
{
var rootVisual = GetComponent<UIDocument>().rootVisualElement;
var userNameField = rootVisual.Q<TextField>("userNameField");
rootVisual.Q<Button>("OkButton").clicked += ()=>{
_completionSource.SetResult(userNameField.text);
}
}
public Awaitable<string> WaitForUsernameAsync() => _completionSource.Awaitable;
}
...
public class HighScoreRanks : MonoBehaviour
{
...
public async Awaitable ReportCurrentUserScoreAsync(int score)
{
_userNameOverlayGameObject.SetActive(true);
var prompt = _userNameOverlayGameObject.GetComponent<UserNamePrompt>();
var userName = await prompt.WaitForUsernameAsync();
_userNameOverlayGameObject.SetActive(false);
await SomeAPICall(userName, score);
}
}