Skip to content

Instantly share code, notes, and snippets.

@antonsmetanin
Created November 18, 2021 14:10
Show Gist options
  • Save antonsmetanin/e613885990f6d4d7062fcc75bafb5614 to your computer and use it in GitHub Desktop.
Save antonsmetanin/e613885990f6d4d7062fcc75bafb5614 to your computer and use it in GitHub Desktop.
Создание тасков

В общем для создания тасков обычно используется следующие способы, в порядке от самых часто используемых к самым редко используемым:

  1. Через ключевое слово 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;
        }
    }
  2. Через создание нового экземпляра 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;
        }
    }
  3. Статический метод 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!");
    });
  4. Через всякие комбинаторы типа 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()
{
    ...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment