В Unity/С# запускается ли .Net async/await, буквально, другой поток?

Важно для любого, кто исследует эту сложную тему в Unity,

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

В частности, в Unity "куда" буквально возвращается ожидание?


Для специалистов по С# Unity является однопоточным 1

Распространено делать вычисления и тому подобное в другом потоке.

Когда вы что-то делаете в другом потоке, вы часто используете async/wait, поскольку все хорошие программисты на С# говорят, что это простой способ сделать это!

void TankExplodes() {

    ShowExplosion(); .. ordinary Unity thread
    SoundEffects(); .. ordinary Unity thread
    SendExplosionInfo(); .. it goes to another thread. let use 'async/wait'
}

using System.Net.WebSockets;
async void SendExplosionInfo() {

    cws = new ClientWebSocket();
    try {
        await cws.ConnectAsync(u, CancellationToken.None);
        ...
        Scene.NewsFromServer("done!"); // class function to go back to main tread
    }
    catch (Exception e) { ... }
}

Итак, когда вы делаете это, вы делаете все "как обычно", когда запускаете поток более обычным способом в Unity/С# (так, используя Thread или что-то еще, или позволяете родному плагину делать это или ОС, или что-то еще дело может быть).

Все отлично работает.

Будучи хромым программистом Unity, который знает достаточно С#, чтобы добраться до конца дня, я всегда предполагал, что вышеприведенный шаблон async/await буквально запускает другой поток.

Фактически, приведенный выше код буквально запускает другой поток, или С#/.NET использует какой-то другой подход для решения задач, когда вы используете шаблон natty async/wait?

Может быть, он работает иначе или конкретно в движке Unity по сравнению с "использованием С# вообще"? (ИДК?)

Обратите внимание, что в Unity то, является ли это потоком, кардинально влияет на то, как вы будете обрабатывать следующие шаги. Отсюда и вопрос.


Проблема: я понимаю, что есть много дискуссий о том, "ждет ли поток", но, (1) я никогда не видел, чтобы это обсуждалось/отвечалось в настройках Unity (это имеет какое-то значение? IDK?) (2) я просто никогда видел четкий ответ!


1 Некоторые вспомогательные вычисления (например, физика и т.д.) Выполняются в других потоках, но фактический "движок игры на основе фреймов" - это один чистый поток. (Невозможно каким-либо образом "получить доступ" к потоку фрейма основного двигателя: при программировании, скажем, собственного плагина или какого-либо вычисления в другом потоке, вы просто оставляете маркеры и значения для компонентов в потоке фрейма двигателя для просмотра и использовать, когда они запускают каждый кадр.)

Ответ 1

Я не люблю отвечать на свой вопрос, но, как оказалось, ни один из ответов здесь не является полностью правильным.

(Однако многие/все ответы здесь очень полезны по-разному, поэтому я отправил им огромные награды.)

Фактически, фактический ответ может быть сформулирован в двух словах:

В каком потоке выполнение возобновляется после того, как ожидание контролируется SynchronizationContext.Current.

Это.

Таким образом, в любой конкретной версии Unity (и обратите внимание, что на момент написания 2019 г. они кардинально меняют Unity - https://unity.com/dots) - или вообще любую среду С#/.NET - вопрос на этой странице можно ответить правильно.

Полная информация появилась в ходе этого последующего контроля качества:

fooobar.com/questions/17209895/...

Ответ 2

Это чтение: задачи (все еще) не являются потоками, и асинхронность не параллельна, может помочь вам понять, что происходит под капотом. Короче говоря, чтобы ваша задача работала в отдельном потоке, вам нужно вызвать

Task.Run(()=>{// the work to be done on a separate thread. }); 

Тогда вы можете ждать этой задачи везде, где это необходимо.

Ответить на ваш вопрос

"На самом деле, приведенный выше код буквально запускает другой поток, или С#/.NET использует какой-то другой подход для решения задач, когда вы используете шаблон natty async/wait?"

Нет, это не так.

Если вы сделали

await Task.Run(()=> cws.ConnectAsync(u, CancellationToken.None));

Затем cws.ConnectAsync(u, CancellationToken.None) будет выполняться в отдельном потоке.

В качестве ответа на комментарий здесь приведен код с дополнительными пояснениями:

    async void SendExplosionInfo() {

        cws = new ClientWebSocket();
        try {
            var myConnectTask = Task.Run(()=>cws.ConnectAsync(u, CancellationToken.None));

            // more code running...
await myConnectTask; // here where it will actually stop to wait for the completion of your task. 
            Scene.NewsFromServer("done!"); // class function to go back to main tread
        }
        catch (Exception e) { ... }
    }

Возможно, вам это не понадобится в отдельном потоке, потому что асинхронная работа, которую вы выполняете, не привязана к процессору (или так кажется). Таким образом, вы должны быть в порядке с

 try {
            var myConnectTask =cws.ConnectAsync(u, CancellationToken.None);

            // more code running...
await myConnectTask; // here where it will actually stop to wait for the completion of your task. 
            Scene.NewsFromServer("done!"); // continue from here
        }
        catch (Exception e) { ... }
    }

Последовательно он будет делать то же самое, что и код выше, но в том же потоке. Это позволит код после "ConnectAsync" для выполнения и будет останавливаться только ждать завершения "ConnectAsync", где он говорит, что ждут и с тех пор "ConnectAsync" не ЦП вы ( что делает его несколько параллельно в смысле работы, Если вы выполняете что-то другое, то есть сетевое взаимодействие), у вас будет достаточно сока для выполнения ваших задач, если только ваш код в "...." также не требует много работы с процессором, которую вы предпочитаете выполнять параллельно.

Также вы можете не использовать async void для него только для функций верхнего уровня. Попробуйте использовать async Task в сигнатуре вашего метода. Вы можете прочитать больше об этом здесь.

Ответ 4

Важное замечание

Прежде всего, есть проблема с первым вопросом вашего вопроса.

Unity однопоточный

Единство не однопоточное; на самом деле Unity является многопоточной средой. Зачем? Просто зайдите на официальную веб-страницу Unity и прочитайте там:

Высокопроизводительная многопоточная система: полностью использовать многоядерные процессоры, доступные сегодня (и завтра), без сложного программирования. Наша новая основа для обеспечения высокой производительности состоит из трех подсистем: система заданий С#, которая дает вам безопасную и простую песочницу для написания параллельного кода; Entity Component System (ECS), модель для написания высокопроизводительного кода по умолчанию, и Burst Compiler, который производит высокооптимизированный собственный код.

Движок Unity 3D использует .NET Runtime под названием "Mono", который по своей природе является многопоточным. Для некоторых платформ управляемый код будет преобразован в собственный код, поэтому среда выполнения .NET не будет. Но сам код в любом случае будет многопоточным.

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

То, с чем вы спорите, это просто утверждение, что в Unity есть основной поток, который обрабатывает основную рабочую нагрузку на основе фреймов. Это правда. Но это не что-то новое и уникальное! Например, приложение WPF, работающее в .NET Framework (или .NET Core, начиная с 3.0), также имеет основной поток (часто называемый потоком пользовательского интерфейса), и рабочая нагрузка обрабатывается в этом потоке на основе фреймов с помощью Dispatcher WPF ( очередь диспетчера, операции, кадры и т.д.) Но все это не делает среду однопоточной! Это просто способ обработки логики приложения.


Ответ на ваш вопрос

Обратите внимание: мой ответ относится только к тем экземплярам Unity, в которых запущена среда .NET Runtime (Mono). Для тех случаев, которые преобразуют управляемый код С# в собственный код C++ и создают/запускают собственные двоичные файлы, мой ответ, скорее всего, по крайней мере, неточный.

Ты пишешь:

Когда вы что-то делаете в другом потоке, вы часто используете async/wait, поскольку все хорошие программисты на С# говорят, что это простой способ сделать это!

Ключевые слова async и await в С# - это просто способ использовать TAP (Task-Asynchronous Pattern).

TAP используется для произвольных асинхронных операций. Вообще говоря, нет темы. Я настоятельно рекомендую прочитать эту статью Стивена Клири под названием "Нет темы". (Стивен Клири - известный гуру асинхронного программирования, если вы не знаете.)

Основной причиной использования функции async/await является асинхронная операция. Вы используете async/await не потому, что "вы что-то делаете в другом потоке", а потому, что у вас есть асинхронная операция, которую вы должны ждать. Независимо от того, существует ли фоновый поток, эта операция будет выполняться или нет - это не имеет значения для вас (ну, почти; см. Ниже). TAP - это уровень абстракции, который скрывает эти детали.

Фактически, приведенный выше код буквально запускает другой поток, или С#/.NET использует какой-то другой подход для решения задач, когда вы используете шаблон natty async/wait?

Правильный ответ: это зависит.

  • если ClientWebSocket.ConnectAsync генерирует исключение проверки аргумента (например, ArgumentNullException когда uri имеет значение null), новый поток не будет запущен
  • если код в этом методе завершается очень быстро, результат метода будет доступен синхронно, новый поток не будет запущен
  • если реализация метода ClientWebSocket.ConnectAsync является чисто асинхронной операцией без участия потоков, ваш вызывающий метод будет "приостановлен" (из-за await), поэтому новый поток не будет запущен
  • если реализация метода включает потоки и текущий TaskScheduler может запланировать этот рабочий элемент в потоке пула потоков, новый поток не будет запущен; вместо этого рабочий элемент будет поставлен в очередь в уже запущенном потоке пула потоков
  • если все потоки пула потоков уже заняты, среда выполнения может порождать новые потоки в зависимости от его конфигурации и текущего состояния системы, поэтому да - новый поток может быть запущен, а рабочий элемент будет поставлен в очередь в этом новом потоке

Видите ли, это довольно сложно. Но это именно та причина, по которой шаблон TAP и пара ключевых слов async/await были введены в С#. Обычно это вещи, которые разработчик не хочет беспокоить, поэтому давайте спрятать этот материал в среде выполнения/фреймворке.

@agfc утверждает, что это не совсем правильно:

"Это не будет запускать метод в фоновом потоке"

await cws.ConnectAsync(u, CancellationToken.None);

"Но это будет"

await Task.Run(()=> cws.ConnectAsync(u, CancellationToken.None));

Если ConnectAsync синхронной части ConnectAsync, планировщик задач может выполнить эту часть синхронно в обоих случаях. Таким образом, оба этих фрагмента могут быть совершенно одинаковыми в зависимости от реализации вызываемого метода.

Обратите внимание, что ConnectAsync имеет суффикс Async и возвращает Task. Это основанная на соглашении информация, что метод действительно асинхронный. В таких случаях вы всегда должны предпочитать await MethodAsync() await Task.Run(() => MethodAsync()).

Дальше интересное чтение:

Ответ 5

Код после ожидания будет продолжен в другом потоке потоков. Это может иметь последствия при работе с не поточно-ориентированными ссылками в методе, таком как Unity, EF DbContext и многих других классах, включая ваш собственный пользовательский код.

Возьмите следующий пример:

    [Test]
    public async Task TestAsync()
    {
        using (var context = new TestDbContext())
        {
            Console.WriteLine("Thread Before Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread Before Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var result = await names;
            Console.WriteLine("Thread After Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

Выход:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async: 29
Thread Before Await: 29
Thread After Await: 12

1 passed, 0 failed, 0 skipped, took 3.45 seconds (NUnit 3.10.1).

Обратите внимание, что код до и после ToListAsync выполняется в одном потоке. Поэтому, прежде чем ожидать каких-либо результатов, мы можем продолжить обработку, хотя результаты асинхронной операции не будут доступны, только созданная Task. (которые могут быть прерваны, ожидаемы и т.д.) После того, как мы await, следующий код будет эффективно отделен как продолжение и может/может вернуться в другой поток.

Это применяется, когда ожидается асинхронная операция в строке:

    [Test]
    public async Task TestAsync2()
    {
        using (var context = new TestDbContext())
        {
            Console.WriteLine("Thread Before Async/Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = await context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread After Async/Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

Выход:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async/Await: 6
Thread After Async/Await: 33

1 passed, 0 failed, 0 skipped, took 4.38 seconds (NUnit 3.10.1).

Опять же, код после ожидания выполняется в другом потоке из оригинала.

Если вы хотите, чтобы код, вызывающий асинхронный код, оставался в одном и том же потоке, вам нужно использовать Result on the Task чтобы заблокировать поток до тех пор, пока асинхронное задание не завершится:

    [Test]
    public void TestAsync3()
    {
        using (var context = new TestDbContext())
        {
            Console.WriteLine("Thread Before Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread After Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var result = names.Result;
            Console.WriteLine("Thread After Result: " + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

Выход:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async: 20
Thread After Async: 20
Thread After Result: 20

1 passed, 0 failed, 0 skipped, took 4.16 seconds (NUnit 3.10.1).

Итак, что касается Unity, EF и т.д., Вы должны быть осторожны при использовании async в любом случае, когда эти классы не являются потокобезопасными. Например, следующий код может привести к неожиданному поведению:

        using (var context = new TestDbContext())
        {
            var ids = await context.Customers.Select(x => x.CustomerId).ToListAsync();
            foreach (var id in ids)
            {
                var orders = await context.Orders.Where(x => x.CustomerId == id).ToListAsync();
                // do stuff with orders.
            }
        }

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

Используйте асинхронный режим, если он дает существенное преимущество по сравнению с синхронными вызовами, и вы уверены, что продолжение получит доступ только к потокобезопасному коду.