В общем для создания тасков обычно используется следующие способы, в порядке от самых часто используемых к самым редко используемым:
-
Через ключевое слово
async
. Это для случая, когда у нас уже имеются какие-то таски или эвэйтеры и мы их просто await'им в методе. То есть, когда мы помечаем метод словомasync
, это превращает его в объект (или структуру) со стейт машиной внутри. Используется везде.Пример:
Task<bool> DoFirstAsync() { ... } Task<bool> DoSecondAsync() { ... } async Task<bool> DoFirstOrElseDoSecond() { var firstSucceeded = await DoFirstAsync(); if (!firstSucceeded) { return await DoSecondAsync(); } else { return true; } }
-
Через создание нового экземпляра
TaskCompletionSource
и взятие у него свойства.Task
. Это для случая, когда у нас есть какая-то функция с колбеком или с ивентом об окончании. Используется относительно часто.Пример:
public class MyWindow : MonoBehaviour { [SerializeField] private Button _okButton; [SerializeField] private InputField _inputField; public Task<string> Show() { gameObject.SetActive(true); var tcs = new TaskCompletionSource<string>(); _okButton.onClick.AddListener(() => { gameObject.SetActive(false); tcs.TrySetResult(_inputField.text); }); return tcs.Task; } }
-
Статический метод
Task.Run()
— это для случая, когда нам нужно выполнить какой-то код в другом потоке, потому что лямбда, переданная вTask.Run()
будет выполнена на тредпуле. В юнити используется довольно редко, потому что юнити не любит, когда пытаешься работать с её компонентами не из главного потока. То есть, это обычно либо какие-то тяжёлые вычисления, либо работа с I/O вроде записи в файл. (Но даже при работе с I/O в последних дотнетах добавили асинхронные версии функций типаFile.WriteAllTextAsync()
, так что это становится ещё менее актуальным.)Примеры:
Task.Run(() => { var sum = 0; for (var i = 0; i < int.MaxValue; i++) { sum += i; } return sum; });
Task.Run(() => { File.WriteAllText(@"C:\huge file.txt", "Hey!"); });
-
Через всякие комбинаторы типа
Task.WhenAll()
иTask.WhenAny()
. Используется очень редко.
Во всех этих примерах, когда мы получаем таск, он уже запущен и мы можем только дожидаться окончания его выполнения.
Если мы хотим выполнить таски параллельно, это будет выглядеть так:
Task DoSomethingAsync(Item item) { ... }
var taskList = new List<Task>();
foreach (var item in list) {
// запускаем таск и добавляем его в список
var task = DoSomethingAsync(item);
taskList.Add(task);
}
// дожидаемся окончания работы всех тасков
await Task.WhenAll(taskList);
Если последовательно, то так:
Task DoSomethingAsync(Item item) { ... }
foreach (var item in list) {
// запускаем таск и дожидаемся окончания его работы
await DoSomethingAsync(item);
}
И как правильно заметили выше, лучше использовать UniTask
из этого пакета:
https://github.com/Cysharp/UniTask
Он работает на структурах и поэтому там нет аллокаций.
Плюс, Task
и UniTask
легко комбинировать, потому что если у нас есть цепочка асихронных методов типа такой:
async Task<int> First()
{
await Second();
}
async Task<int> Second()
{
await Third();
}
async Task<int> Third()
{
...
}
, каждый новый метод — это отдельная стейт машина, которая работает независимо от других, и каждый вызов такого метода создаёт отдельный таск.
В итоге мы вполне можем сделать первый и третий UniTask
'ами, а второй оставить Task
'ом:
async UniTask<int> First()
{
await Second();
}
async Task<int> Second()
{
await Third();
}
async UniTask<int> Third()
{
...
}
Или наоборот:
async Task<int> First()
{
await Second();
}
async UniTask<int> Second()
{
await Third();
}
async Task<int> Third()
{
...
}