Обновление: в 2018 году Unity внедряет систему заданий C # как способ разгрузить работу и использовать несколько ядер ЦП.
Ответ ниже предшествует этой системе. Это все еще будет работать, но в современном Unity могут быть и другие варианты, в зависимости от ваших потребностей. В частности, система заданий, по-видимому, устраняет некоторые ограничения на то, что потоки, созданные вручную, могут безопасно получить доступ, описанные ниже. Например, разработчики экспериментируют с отчетом о предварительном просмотре, выполняя raycast и создавая сетки параллельно .
Я бы пригласил пользователей с опытом работы с этой системой заданий добавить свои собственные ответы, отражающие текущее состояние движка.
В прошлом я использовал многопоточность для тяжеловесных задач в Unity (обычно это обработка изображений и геометрии), и она не сильно отличается от использования потоков в других приложениях C # с двумя оговорками:
Поскольку в Unity используется несколько более старое подмножество .NET, есть некоторые новые функции и библиотеки потоков, которые мы не можем использовать "из коробки", но здесь есть основы.
Как отмечает Almo в комментарии выше, многие типы Unity не являются потокобезопасными и будут генерировать исключения, если вы попытаетесь сконструировать, использовать или даже сравнить их из основного потока. Что нужно иметь в виду:
Один из распространенных случаев - проверка на наличие нулевой ссылки GameObject или Monobehaviour, прежде чем пытаться получить доступ к его членам. myUnityObject == null
вызывает перегруженный оператор для всего, что происходит от UnityEngine.Object, но System.Object.ReferenceEquals()
работает в некоторой степени вокруг этого - просто помните, что редактируемый GameObject Destroy () сравнивается как равный null, используя перегрузку, но еще не ReferenceEqual для null.
Чтение параметров из типов Unity обычно безопасно в другом потоке (в том смысле, что оно не вызовет немедленного исключения, если вы будете тщательно проверять наличие нулевых значений, как указано выше), но обратите внимание на предупреждение Филиппа о том, что основной поток может изменять состояние пока ты читаешь это. Вы должны будете быть дисциплинированными в отношении того, кому разрешено изменять что и когда, чтобы избежать чтения какого-либо противоречивого состояния, которое может привести к ошибкам, которые могут быть чертовски трудно отследить, поскольку они зависят от временных интервалов между нитями, которые мы можем Воспроизведение по желанию.
Случайные и статические члены Time недоступны. Создайте экземпляр System.Random для каждого потока, если вам нужна случайность, и System.Diagnostics.Stopwatch, если вам нужна информация о времени.
Все функции Mathf, Vector, Matrix, Quaternion и Color хорошо работают между потоками, поэтому вы можете выполнять большинство вычислений отдельно.
Создание GameObjects, присоединение Monobehaviours или создание / обновление текстур, сеток, материалов и т. Д. - все это должно происходить в главном потоке. В прошлом, когда мне нужно было работать с ними, я настраивал очередь производителя-потребителя, где мой рабочий поток подготавливает необработанные данные (например, большой массив векторов / цветов для применения к сетке или текстуре), и обновление или сопрограмма в главном потоке опрашивает данные и применяет их.
С этими заметками, вот шаблон, который я часто использую для многопоточной работы. Я не гарантирую, что это стиль наилучшей практики, но он выполняет свою работу. (Комментарии или изменения для улучшения приветствуются - я знаю, что многопоточность - очень глубокая тема, из которой я знаю только основы)
using UnityEngine;
using System.Threading;
public class MyThreadedBehaviour : MonoBehaviour
{
bool _threadRunning;
Thread _thread;
void Start()
{
// Begin our heavy work on a new thread.
_thread = new Thread(ThreadedWork);
_thread.Start();
}
void ThreadedWork()
{
_threadRunning = true;
bool workDone = false;
// This pattern lets us interrupt the work at a safe point if neeeded.
while(_threadRunning && !workDone)
{
// Do Work...
}
_threadRunning = false;
}
void OnDisable()
{
// If the thread is still running, we should shut it down,
// otherwise it can prevent the game from exiting correctly.
if(_threadRunning)
{
// This forces the while loop in the ThreadedWork function to abort.
_threadRunning = false;
// This waits until the thread exits,
// ensuring any cleanup we do after this is safe.
_thread.Join();
}
// Thread is guaranteed no longer running. Do other cleanup tasks.
}
}
Если вам не нужно строго распределять работу между потоками для ускорения, и вы просто ищете способ сделать его неблокирующим, чтобы остальная часть игры продолжала работать, более легким решением в Unity является Coroutines . Это функции, которые могут выполнять некоторую работу, а затем возвращать управление движку для продолжения работы и возобновления работы в более позднее время.
using UnityEngine;
using System.Collections;
public class MyYieldingBehaviour : MonoBehaviour
{
void Start()
{
// Begin our heavy work in a coroutine.
StartCoroutine(YieldingWork());
}
IEnumerator YieldingWork()
{
bool workDone = false;
while(!workDone)
{
// Let the engine run for a frame.
yield return null;
// Do Work...
}
}
}
Для этого не нужны особые соображения по очистке, поскольку движок (насколько я могу судить) избавляет вас от сопрограмм от разрушенных объектов.
Все локальное состояние метода сохраняется при его выходе и возобновлении, поэтому для многих целей он работает так, как будто он работает непрерывно в другом потоке (но у вас есть все удобства работы в основном потоке). Вам просто нужно убедиться, что каждая его итерация достаточно коротка, чтобы не замедлять основной поток.
Убедившись, что важные операции не разделены выходом, вы можете получить согласованность однопоточного поведения - зная, что никакой другой скрипт или система в основном потоке не может изменить данные, над которыми вы работаете.
Линия возврата урожая дает вам несколько вариантов. Вы можете...
yield return null
возобновить после обновления следующего кадра ()
yield return new WaitForFixedUpdate()
возобновить после следующего FixedUpdate ()
yield return new WaitForSeconds(delay)
возобновить игру по истечении определенного количества игрового времени
yield return new WaitForEndOfFrame()
возобновить после того, как GUI закончит рендеринг
yield return myRequest
где myRequest
это WWW экземпляр, чтобы возобновить когда запрошенные данные после загрузки из Интернета или диска.
yield return otherCoroutine
где otherCoroutine
это экземпляр сопрограммная , чтобы возобновить после otherCoroutine
Завершает. Это часто используется в форме, yield return StartCoroutine(OtherCoroutineMethod())
чтобы связать выполнение с новой сопрограммой, которая сама может дать результат, когда захочет.
Экспериментально, пропуская вторую StartCoroutine
и простую запись, можно yield return OtherCoroutineMethod()
достичь одной и той же цели, если вы хотите связать выполнение в одном контексте.
Обтекание внутри StartCoroutine
может по-прежнему быть полезным, если вы хотите запустить вложенную сопрограмму вместе со вторым объектом, напримерyield return otherObject.StartCoroutine(OtherObjectsCoroutineMethod())
... в зависимости от того, когда вы хотите, чтобы сопрограмма сделала следующий ход.
Или yield break;
остановить сопрограмму до того, как она достигнет конца, как вы могли бы использовать return;
для раннего выхода из обычного метода.