← Назад к вопросам

Как работает многопоточность внутри Unity?

2.7 Senior🔥 231 комментариев
#Unity Core#Асинхронность и многопоточность#Управление памятью

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Отличный вопрос, который затрагивает самую суть архитектуры Unity. Короткий ответ: Unity, за редкими исключениями, не является потокобезопасной (thread-safe) и использует однопоточную модель для работы с движком и игровыми объектами. Давайте разберем это детально.

Основной принцип: Главный поток (Main Thread)

Весь игровой цикл (Update, FixedUpdate, рендеринг, физика, обработка ввода) выполняется в одном потоке, называемом главным потоком (Main Thread) или иногда "потоком игры". Это архитектурное решение, унаследованное от дизайна первоначального движка, и оно имеет ключевые преимущества:

  • Простота: Упрощает внутреннюю логику движка, избавляет от необходимости синхронизации.
  • Детерминизм: Поведение игры более предсказуемо и воспроизводимо.
  • Безопасность: Разработчику не нужно думать о взаимных блокировках (deadlocks) или состояниях гонки (race conditions) при работе с API Unity.

ВАЖНО: Почти все API Unity (например, Transform.position, GameObject.Instantiate, доступ к компонентам) можно вызывать только из главного потока. Попытка сделать это из другого потока вызовет ошибку в редакторе или сбой в сборке.

Где и как используется многопоточность?

Несмотря на однопоточность ядра, Unity активно использует фоновые потоки для тяжелых вычислений, чтобы не блокировать главный поток и не вызывать фризов (зависаний кадра).

1. Job System и Burst Compiler (современный подход)

Начиная с версии 2018.3, Unity представила C# Job System и Burst Compiler — высокопроизводительную систему для распараллеливания задач.

  • Job System: Позволяет создавать небольшие единицы работы (джобы), которые можно безопасно выполнять на нескольких рабочих потоках. Джобы организуются в зависимости (dependencies) и планируются системой.
  • Burst Compiler: Транслирует код джоб, написанный на безопасном подмножестве C#, в высокооптимизированный машинный код, что дает огромный прирост производительности (часто в 2-5 раз).
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;

public class ParallelCalculationExample : MonoBehaviour
{
    void Start()
    {
        // Используем NativeArray для безопасной работы с памятью между потоками
        var numbers = new NativeArray<float>(1000, Allocator.TempJob);
        var results = new NativeArray<float>(1000, Allocator.TempJob);

        // Заполняем массив
        for (int i = 0; i < numbers.Length; i++) numbers[i] = i;

        // Создаем джобу, которая будет выполняться в параллельных потоках
        var job = new CalculationJob
        {
            Input = numbers,
            Output = results
        };

        // Планируем выполнение джобы, указываем размер "батча" для распараллеливания
        JobHandle handle = job.Schedule(results.Length, 64);

        // Ждем завершения джобы (можно делать другие задачи в главном потоке)
        handle.Complete();

        // Теперь результаты доступны и мы можем использовать их в главном потоке
        Debug.Log($"Result sample: {results[42]}");

        // Обязательно освобождаем неуправляемую память
        numbers.Dispose();
        results.Dispose();
    }

    // Определение структуры джобы
    struct CalculationJob : IJobParallelFor
    {
        [ReadOnly] public NativeArray<float> Input;
        [WriteOnly] public NativeArray<float> Output;

        public void Execute(int index)
        {
            // Эта функция выполняется МНОЖЕСТВОМ потоков!
            // Разным потокам приходят разные значения index.
            Output[index] = Mathf.Sqrt(Input[index]) * 10f;
        }
    }
}

2. Система физики (Physics Engine)

Начиная с определенных версий, Unity может выполнять вычисления физики (Physics) в отдельном потоке по умолчанию. Это означает, что FixedUpdate (логика) и расчет столкновений/движения могут происходить параллельно. Однако результаты все равно синхронизируются и применяются к GameObject в главном потоке.

3. Асинхронные операции (Async Operations)

Операции, которые потенциально могут быть долгими, часто имеют асинхронные варианты:

  • AssetBundle.LoadAssetAsync
  • Resources.LoadAsync
  • Addressables
  • SceneManager.LoadSceneAsync

Эти операции выполняют часть работы (например, чтение с диска, декомпрессию) в фоновых потоках, но финальное создание объектов, привязка ассетов и вызов колбэков (например, AsyncOperation.completed) происходят в главном потоке.

4. Пользовательские потоки (Thread)

Вы можете создавать свои собственные потоки (System.Threading.Thread) или использовать Task из TPL (Task Parallel Library) для:

  • Сетевых запросов (WebRequest, Socket).
  • Сложных математических вычислений (генерация мира, pathfinding на графах).
  • Работы с файловой системой.

Критически важно: Любые данные, полученные в фоновом потоке, должны быть переданы в главный поток для взаимодействия с движком. Для этого используются:

  • Очередь действий (Action Queue): Сохраняете действия в потокобезопасную очередь и выполняете ее в Update.
  • UnityEngine.UnitySynchronizationContext: Можно использовать await в контексте главного потока.
  • MainThreadDispatcher (сторонние ассеты): Готовые решения для вызова методов в главном потоке.
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class BackgroundWorkerExample : MonoBehaviour
{
    // Потокобезопасная очередь для хранения действий
    private readonly ConcurrentQueue<System.Action> _mainThreadActions = new();

    void Start()
    {
        // Запускаем тяжелую задачу в фоновом потоке
        Task.Run(() => HeavyComputationInBackground());
    }

    void Update()
    {
        // В главном потоке, в каждом кадре, проверяем и выполняем все накопленные действия
        while (_mainThreadActions.TryDequeue(out var action))
        {
            action?.Invoke();
        }
    }

    void HeavyComputationInBackground()
    {
        Thread.Sleep(1000); // Имитация долгого расчета
        float result = 42.0f;

        // НЕЛЬЗЯ: transform.position = new Vector3(result, 0, 0);

        // МОЖНО: Ставим задачу в очередь на выполнение в главном потоке
        _mainThreadActions.Enqueue(() =>
        {
            // Этот код уже выполняется в главном потоке
            transform.position = new Vector3(result, 0, 0);
            Debug.Log($"Position set from background thread result: {result}");
        });
    }
}

Итог и лучшие практики

  1. Главный поток — святая святых: Вся игровая логика, манипуляции с объектами и рендеринг живут здесь.
  2. Используйте Job System/Burst для вычислений: Это самый эффективный и безопасный способ распараллеливания внутри Unity.
  3. Собственные потоки — для I/O и внешних вычислений: Сеть, файлы, сложные алгоритмы, не зависящие от API Unity.
  4. Синхронизация обязательна: Любое взаимодействие с UnityEngine.Object должно происходить в главном потоке. Планируйте передачу данных заранее.

Понимание этой модели — ключ к написанию производительных и стабильных игр в Unity, позволяющих эффективно использовать многоядерные процессоры без ущерба для стабильности.