В Unity, скажем, у вас есть GameObject
. Таким образом, это может быть Лара Крофт, Марио, злая птица, определенный куб, определенное дерево или что-то еще.
(Напомним, что Unity - это не OO, это ECS. Сами Component
которые вы можете "прикрепить" к GameObject
могут создаваться или не создаваться на языке OO, но сам Unity - это просто список GameObject
и движок фреймов, который запускает любой Component
на каждом кадре. Таким образом, действительно, Unity, конечно, "совершенно" однопоточный, даже нет концептуального способа сделать что-либо, связанное с "фактическим Unity" ("списком игровых объектов") в другом 1 потоке.)
Так, скажем, на кубе у нас есть Component
под названием Test
public class Test: MonoBehaviour {
У него есть псевдофункция Update, поэтому Unity знает, что мы хотим запускать что-то каждый кадр.
private void Update() { // this is Test Update call
Debug.Log(ManagedThreadId); // definitely 101
if (something) DoSomethingThisParticularFrame();
}
Допустим, единство потока "101".
Таким образом, это Обновление (и действительно любое Обновление любого фрейма на любом игровом объекте) напечатает 101.
Поэтому время от времени, возможно, каждые несколько секунд по какой-то причине мы выбираем запуск DoSomethingThisFrame
.
Таким образом, каждый кадр (очевидно, в "потоке Unity... есть/может быть только один поток) Unity выполняет все вызовы Update для различных игровых объектов.
Итак, на одном конкретном кадре (скажем, 24-м кадре из 819-й секунды игрового процесса), допустим, он запускает DoSomethingThisParticularFrame
для нас.
void DoSomethingThisParticularFrame() {
Debug.Log(ManagedThreadId); // 101 I think
TrickyBusiness();
}
Я предполагаю, что это также напечатает 101.
async void TrickyBusiness() {
Debug.Log("A.. " + ManagedThreadId); // 101 I think
var aTask = Task.Run(()=>BigCalculation());
Debug.Log("B.. " + ManagedThreadId); // 101 I think
await aTask;
Debug.Log("C.. " + ManagedThreadId); // In Unity a mystery??
ExplodeTank();
}
void BigCalculation() {
Debug.Log("X.. " + ManagedThreadId); // say, 999
for (i = 1 to a billion) add
}
Итак
Я уверен, что в А это напечатает 101. Я думаю.
Я думаю, что в B это будет печатать 101
Я верю, но я не уверен, что в X он запустит другой поток для BigCalculation. (Скажите, 999.) (Но, может быть, это не так, кто знает.)
Какой поток мы на C, где он (пытается?) Взорвать танк????
(Например, рассмотрите этот превосходный ответ и обратите внимание на первый пример вывода "Thread After Await: 12". 12 отличается от 29.)
Но это бессмысленно в Unity -
... как TrickyBusiness
может быть в "другом потоке" - что бы это значило, что вся сцена дублирована, или?
Или это так (в Unity особенно и только? IDK),
в момент, когда начинается TrickyBusiness
, Unity фактически помещает это (что - голый экземпляр класса "Test"??) в другой поток?
await
что печатается на C или A?Казалось бы, что:
1 Очевидно, что некоторые вспомогательные вычисления (например, рендеринг и т.д.) Выполняются на других ядрах, но фактический "игровой движок на основе фреймов" является одним чистым потоком. (Невозможно каким-либо образом "получить доступ" к потоку фрейма основного движка: когда вы программируете, скажем, собственный плагин или какой-то расчет, выполняющийся в другом потоке, все, что вы можете сделать, это оставить маркеры и значения для компонентов на нить рамы двигателя, чтобы посмотреть и использовать, когда они запускают каждый кадр.)
Async как абстракция высокого уровня не имеет отношения к потокам.
В каком потоке выполнение возобновляется после того, как await
контролируется System.Threading.SynchronizationContext.Current
.
Например, WindowsFormsSynchronizationContext
обеспечит возобновление выполнения, запущенного в потоке графического интерфейса, в потоке графического интерфейса после await
, поэтому, если вы выполните тест в приложении WinForms, вы увидите, что ManagedThreadId
остается таким же после await
.
Например, AspNetSynchronizationContext
не заботится о сохранении потоков и позволит возобновить код в любом потоке.
Например, ASP.NET Core вообще не имеет контекста синхронизации.
Что бы ни случилось в Unity, зависит от того, что он имеет в качестве SynchronizationContext.Current
. Вы можете проверить, что он возвращает.
Выше приведено "достаточно верное" представление событий, то есть того, что вы можете ожидать от своего обычного скучного повседневного асинхронного/ожидающего кода, связанного с обычными функциями Task<T>
которые возвращают свои результаты обычным способом.
Вы абсолютно можете настроить эти поведения:
Вы можете отказаться от захвата контекста , вызвав ConfigureAwait(false)
со своими ожиданиями. Поскольку контекст не фиксируется, все, что идет с контекстом, теряется, включая возможность возобновления в исходном потоке (для контекстов, связанных с потоками).
Вы можете разработать асинхронный код, который намеренно переключает вас между потоками, даже если вы не используете ConfigureAwait(false)
. Хороший пример можно найти в блоге Рэймонда Чена (часть 1, часть 2) и показывает, как явно перейти на другой поток в середине метода с помощью
await ThreadSwitcher.ResumeBackgroundAsync();
а затем вернуться с
await ThreadSwitcher.ResumeForegroundAsync(Dispatcher);
Поскольку весь механизм async/await слабо связан (вы можете await
любой объект, который определяет метод GetAwaiter()
), вы можете создать объект, для которого GetAwaiter()
делает все, что вам нужно, с текущим потоком/контекстом (фактически, это это именно то, что вышеупомянутый пункт пули).
SynchronizationContext.Current
волшебным образом не навязывает свой код другим людям: он работает наоборот. SynchronizationContext.Current
действует только потому, что реализация Task<T>
выбирает его соблюдение. Вы можете реализовать другое ожидание, которое игнорирует его.
ConfigureAwait
также является фактором, влияющим на это. Кроме того, async/await
- это просто абстракция, и вы можете создать свой собственный awaiter / awaitable, который будет всегда возобновляться в исходном потоке или просто игнорировать контекст синхронизации вообще.