Асинхронная функция в Unity
Асинхронное и ожидающее ключевое слово
Начиная с Unity 2019, Unity представляет задачу C# и ключевое слово async
/await
для MonoBehaviour. Для функций обратного вызова Unity, таких как Start
, Update
, теперь поддерживается асинхронная версия, а с ключевым словом async
в начале функция теперь будет автоматически отправляться движком асинхронно.
private async void Start()
{
Debug.Log("Start task delay 2 seconds");
await Task.Delay(TimeSpan.FromSeconds(2));
Debug.Log("Task delay 2 finished");
}
Функция, указанная выше, будет выполнена, и первый журнал появится сразу, а второй журнал появится через 2 секунды.
Встроенная HTTP-библиотека C# также предоставляет удобную асинхронную оболочку, которую также можно использовать в асинхронном MonoBehaviour.
private async void Start()
{
var httpClient = new System.Net.Http.HttpClient();
var resp = await httpClient.GetAsync("https://google.com");
Debug.Log(resp.StatusCode);
}
С помощью волшебной клавиатуры async
мы можем заменить стандартную сопрограмму Unity, реализованную генератором yield
, на более чистый и приятный стиль программирования.
Проблема с фоновой задачей
Однако функция async
, очень похожая на JavaScript, не выполняется в другом потоке. Вместо этого функция выполняется в основном потоке, и только когда появляется ключевое слово await
, функция может выполняться или МОЖЕТ НЕ в основном потоке.
private async void Start()
{
Log("Start", "Task delay 2 seconds");
await Task.Delay(TimeSpan.FromSeconds(2));
Log("Start", "Task delay 2 finished");
Log("Start", "Thread sleep 2 seconds");
Thread.Sleep(TimeSpan.FromSeconds(2));
Log("Start", "Thread sleep done");
}
private void Update()
{
_frames++;
}
private void Log(string caller, string message)
{
Debug.Log($"[Thread {Thread.CurrentThread.ManagedThreadId}][Frame {_frames}][Method {caller}] {message}");
}
Здесь мы определяем вспомогательную функцию Log
для отслеживания текущего потока и текущего количества кадров. Приведенная выше функция возвращает:
1. [Thread 1][Frame 0][Method Start] Task delay 2 seconds
2. [Thread 1][Frame 172][Method Start] Task delay 2 finished
3. [Thread 1][Frame 172][Method Start] Thread sleep 2 seconds
4. [Thread 1][Frame 172][Method Start] Thread sleep done
Обратите внимание, что функция выполняется последовательно и асинхронно. Строка 1 и строка 2 показывают, что ключевое слово await
переводит задачу в фоновый режим и возобновляет выполнение через 2 секунды, поскольку количество кадров отличается. Однако строки 3 и 4 показывают, что функция все еще работает в основном потоке, и если в функции есть сложная вычислительная задача, она блокирует всю систему до завершения.
Обходной путь
Вместо того, чтобы запускать задачу в основном потоке, мы можем принудительно запустить ее в фоновом потоке и вернуть задачу, чтобы сообщить основному потоку об ожидании результата.
private async void Start()
{
Log("Start", "Task delay 2 seconds");
await Task.Delay(TimeSpan.FromSeconds(2));
Log("Start", "Task delay 2 finished");
Log("Start", "Thread sleep 2 seconds");
await Task.Run(() => Thread.Sleep(TimeSpan.FromSeconds(2)));
Log("Start", "Thread sleep done");
}
Task.Run
запустит лямбда-действие в фоновом потоке. Вместо того, чтобы блокировать выполнение основного потока, функция основного потока Start
приостанавливается здесь и ждет завершения функции Task.Run
. Затем он возобновляется позже. Приведенная выше функция возвращает:
1. [Thread 1][Frame 0][Method Start] Task delay 2 seconds
2. [Thread 1][Frame 167][Method Start] Task delay 2 finished
3. [Thread 1][Frame 167][Method Start] Thread sleep 2 seconds
4. [Thread 1][Frame 378][Method Start] Thread sleep done
Однако это не идеально. Ключевое слово Task.Run
требует, чтобы код был заключен в функцию, что прерывает последовательный поток кода и делает язык более подробным. Во-вторых, теперь он работает с магическими ключевыми словами сопрограммы Unity
, такими как WaitForEndOfFrame
.
UniTask решает эти проблемы элегантным и эффективным способом!
ЮниТаск
UniTask (https://github.com/Cysharp/UniTask) написан Йошифуми Каваи, японским разработчиком и Microsoft MVP для Visual C# с 2011 года. Целью UniTask является обеспечение эффективной интеграции async/await в Unity без выделения ресурсов и Задача на основе PlayerLoop (UniTask.Yield
, UniTask.Delay
, UniTask.DelayFrame
и т. д.), которая позволяет заменить все операции сопрограммы.
UniTask предоставляет замену стандартной библиотеки задач C# и стандартной сопрограммы Unity по принципу plug-and-play. Например,
private IEnumerator<object> CoroutineSleep()
{
Log("Coroutine", "Coroutin sleep 2 seconds");
yield return new WaitForSeconds(2);
Log("Coroutine", "Coroutin sleep done");
}
Приведенный выше код можно легко преобразовать в версию UniTask.
private async UniTask UniTaskSleep()
{
Log("Coroutine", "UniTask sleep 2 seconds");
await UniTask.Delay(TimeSpan.FromSeconds(2));
Log("Coroutine", "UniTask sleep done");
}
UniTask предоставляет задачи на основе PlayerLoop, например, await UniTask.WaitForEndOfFrame();
, await UniTask.NextFrame();
, чтобы вы могли применять их так же, как в контексте сопрограммы.
Фоновая задача в UniTask
UniTask не только является лучшей заменой сопрограммы Unity, но и обеспечивает более удобное управление фоновыми потоками. С помощью специальной задачи SwitchToThreadPool
UniTask позволяет потоку изменить текущий контекст на поток пула потоков и выполняться в фоновом режиме.
private async void Start()
{
Log("Start", "Task delay 2 seconds");
await UniTask.Delay(TimeSpan.FromSeconds(2));
Log("Start", "Task delay 2 finished");
Log("Start", "Thread sleep 2 seconds");
await UniTask.SwitchToThreadPool();
Log("Start", "Going to sleep");
Thread.Sleep(TimeSpan.FromSeconds(2));
await UniTask.SwitchToMainThread();
Log("Start", "Thread sleep done");
}
Результат вышеуказанной функции
1. [Thread 1][Frame 0][Method Start] Task delay 2 seconds
2. [Thread 1][Frame 167][Method Start] Task delay 2 finished
3. [Thread 1][Frame 167][Method Start] Thread sleep 2 seconds
4. [Thread 68][Frame 168][Method Start] Going to sleep
5. [Thread 1][Frame 287][Method Start] Thread sleep done
UniTask передаст текущий контекст выполнения в пул потоков после UniTask.SwitchToThreadPool()
и переключится обратно на основной поток после UniTask.SwitchToMainThread()
.
UniTask также предоставляет удобный инструмент UniTask Tracker для отслеживания использования задач в редакторе Unity.