Что такое async в контексте корутин (Coroutines)?

Ответ

В Unity под "корутинами" (Coroutines) традиционно понимаются методы, возвращающие IEnumerator и использующие yield return для приостановки выполнения на определенный срок или до события. Ключевое слово async в C# представляет собой альтернативную, более мощную и современную модель асинхронного программирования, основанную на Task и Task<T>, которую также можно использовать в Unity.

Сравнение подходов:

Аспект Coroutines (IEnumerator) async / await
Основа Механизм Unity, тесно связан с циклом Update. Языковая и платформенная фича .NET.
Приостановка yield return new WaitForSeconds(1) await Task.Delay(1000)
Ожидание ресурса Нет нативной поддержки, нужны костыли. Прямая поддержка: await webRequest.SendWebRequest()
Возврат значения Через callback-стиль или глобальные переменные. Прямой возврат значения: var result = await operation
Обработка ошибок try/catch не работает с yield. Полноценная обработка через try/catch.
Контекст потока Всегда выполняется в основном потоке. По умолчанию возвращает управление в контекст синхронизации (основной поток в Unity).

Практический пример замены корутины на async:

using UnityEngine;
using System.Threading.Tasks;

public class AsyncExample : MonoBehaviour
{
    // Классическая корутина в Unity
    private IEnumerator LoadDataCoroutine()
    {
        Debug.Log("Загрузка началась...");
        yield return new WaitForSeconds(2f); // Блокирует основной поток на 2 секунды?
        Debug.Log("Данные загружены (через 2 сек).");
    }

    // Эквивалент с использованием async/await
    private async Task LoadDataAsync()
    {
        Debug.Log("Async загрузка началась...");
        await Task.Delay(2000); // Не блокирует основной поток!
        Debug.Log("Данные загружены асинхронно (через 2 сек).");
        // Можно безопасно работать с GameObject и компонентами здесь.
        gameObject.GetComponent<Renderer>().material.color = Color.green;
    }

    private async void Start() // Обработчики в Unity могут быть `async void`
    {
        // Запуск классической корутины
        StartCoroutine(LoadDataCoroutine());

        // Запуск асинхронной операции
        await LoadDataAsync();
        Debug.Log("Все операции завершены.");
    }
}

Рекомендация: Используйте async/await для операций, связанных с вводом-выводом (I/O), сетевыми запросами или длительными вычислениями, которые можно вынести в фоновый поток. Используйте корутины для последовательностей, жестко привязанных к кадрам (пошаговая анимация, плавное перемещение объекта между кадрами).

Ответ 18+ 🔞

А, ну ты глянь, опять про эти корутины в Unity спрашивают. Ну что ж, давай разберёмся, как это всё работает, а то народ путается, как слепой кот в сортире.

Вот смотри, в Unity есть эти старые добрые корутины — методы, которые возвращают IEnumerator и юзают yield return. Это типа как поставить видео на паузу, а потом продолжить с того же места. Работает через движковый цикл Update, всё в основном потоке.

А теперь появилась штука поинтереснее — async/await. Это уже не Unity-специфичная фигня, а нормальная языковая возможность C#. Основана на Task — задачах, которые можно выполнять асинхронно, не блокируя основной поток, как корутины.

Короче, в чём разница:

Что сравниваем Корутины (IEnumerator) async / await
Откуда взялись Из недр Unity, привязаны к кадрам. Из C# и .NET, как родная.
Как приостановить yield return new WaitForSeconds(1) await Task.Delay(1000)
Ждать загрузки чего-то Костыли, велосипеды, боль и страдание. Прямо в лоб: await webRequest.SendWebRequest()
Вернуть результат Через доп. переменные или колбэки — пиздец какой гемор. Просто возвращаешь значение: var data = await LoadShit()
Поймать ошибку try/catch нихуя не работает с yield. Работает как часы, лови на здоровье.
Где выполняется Только в основном потоке. По умолчанию — тоже в основном, но можно и в фоне, если очень надо.

Пример, чтобы было понятнее:

Вот как это выглядит в коде. Раньше писали так — корутиной:

using UnityEngine;
using System.Collections;

public class Example : MonoBehaviour
{
    private IEnumerator OldSchoolCoroutine()
    {
        Debug.Log("Начинаем загрузку...");
        yield return new WaitForSeconds(2f); // Тут поток на 2 секунды встаёт колом
        Debug.Log("Всё загрузилось, через 2 секунды.");
    }

    void Start()
    {
        StartCoroutine(OldSchoolCoroutine());
    }
}

А теперь можно так — через async:

using UnityEngine;
using System.Threading.Tasks;

public class Example : MonoBehaviour
{
    private async Task ModernAsyncMethod()
    {
        Debug.Log("Async загрузка поехала...");
        await Task.Delay(2000); // А тут поток НЕ блокируется, Unity живёт своей жизнью
        Debug.Log("Всё загрузилось асинхронно, тоже через 2 секунды.");
        // И да, тут можно спокойно менять цвет, позицию — всё как обычно.
        gameObject.GetComponent<Renderer>().material.color = Color.red;
    }

    private async void Start() // Да, Start может быть async void
    {
        await ModernAsyncMethod();
        Debug.Log("И всё, приехали.");
    }
}

Так что же выбрать, спросишь ты?

Если тебе нужно просто поперемещать объект плавно между кадрами или сделать простую последовательность действий, привязанную к времени в игре — корутины ещё вполне себе ничего. Они предсказуемы и все их знают.

А если дело касается сетевых запросов, чтения файлов, долгих вычислений или любой другой хуйни, которая может тормозить основной поток — async/await просто бомба. Современно, удобно, и ошибки ловить можно.

Короче, бери async на вооружение, не ошибёшься. А корутины пусть остаются для простых анимаций и прочей мелочи.