Асинхронная функция в 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.