В 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 в сигнатуре вашего метода. Вы можете прочитать больше об этом здесь.
Ответ 3
Нет, async/await не означает - другой поток. Он может запустить другой поток, но это не обязательно.
Здесь вы можете найти довольно интересный пост об этом: https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/
Ответ 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 будет работать в другом потоке, когда она запрашивается для заказов на основе ожидания на начальной загрузке клиента.
Используйте асинхронный режим, если он дает существенное преимущество по сравнению с синхронными вызовами, и вы уверены, что продолжение получит доступ только к потокобезопасному коду.