Добавлен , опубликован

Суть проблемы

А вся суть в том, что Unity - движок потоко не безопасный. Основную часть его апи не получится вызвать из других потоков, хотя с запуском стандартных C# потоков проблем не наблюдается. И ассинхронная загрузка данных, даже самостоятельная, вполне реальна. Но стоит попытаться вызвать создание объекта, пересчитать вершины меша или другие многие стандартные действия Unity - мы получаем отказ в виде варнинга или даже эксепшна.
Мне в моем проекте было необходимо запускать треды, так как важно было запускать тяжелый по времени цикл, не разбивая его на кадры. Да и сам факт его укладывания в кадр, два или три по времени тоже не был важен... запуск его в Update выдавал весьма плачевный результат... а именно лютую просадку FPS, что логично.
В чем собственно суть, почему в юнити это закрыто? Ну все просто. Представьте, что у вас в игре сталкивается 5 шариков, обсчитывает все физический движок после исполнения FixedUpdate ... и тут, левый поток меняет координаты одного из них, и что мы получаем? Физика уже считается, но шарика на нужном месте нет, он есть в другом, возникают новые колизии, которые, возможно движок и разрулит, но вряд ли. Таким образом мы в лучшем случае получим бардак с физикой на экране, в худшем мы получим вылет билда. И так почти со всем юнити апи, каждая функция, метод имеют место для вызова в общем пайплайне и не должны вызываться вне него.
Авторы движка вполне могли сделать юнити потокобезопасным. Но для этого им пришлось бы повозится с собственной реализацией мультитред апи. К примеру они давали бы людям свою реализацию тредов и приостанавливали бы их в критичных местах... что по сути возможно, но сложно и идеалогически неверно...

Как можно это решить

Тем кто напишет "юзай куротины" - читать дальше не советую, вы не понимаете сути мультитрединга и работы куротин юнити впринципе. Тем кому стало интересно, расскажу суть своего подхода, не претендующего на лавры лучшего, имеющего узкие места, но тем не менее дающего нам почти мультитрединг (во всем, где не юзаются юнити апи, где они используются, приходится немного терять во времени). Я не буду давать готовые классы, дам только куски кода (опишусь сразу, в системе у меня все намного сложнее, идею я пишу на лету и просто для примера. Так как компиляцию это все не проходило, могут быть ошибки... воспринимайте как псевдокод.).
  1. Создаем свою компоненту вот с таким полями:
        object sync = new object();
        List<Action> actions = new List<Action>();        
в Update вписываем следующее:
void Update(){
        lock(sync){ //обеспечиваем потокобезопасность чтения листа
            while(actions.Count!=0){ //и исполняем все действия
                actions[0].Invoke();
                actions.RemoveAt(0);
            }
        }
}
Так же создадим вот такой метод в этом же компоненте:
public void Execute(Action action){
        lock(sync){ //обеспечиваем потокобезопасность записи в лист
            actions.Add(action);
        }
        try{
	      Thread.Sleep ();//усыпляем вызвавший поток
        }catch(ThreadInterruptedException){
	    }finally{}        
}
Данный метод нельзя вызывать из основного потока, ни в коем случае.
  1. По сути автомат для исполнения у нас есть, как же нам нужно запускать треды? А вот так:
//customComponent далее это экземпляр вашего компонента

Thread testThread = null;

Action threadAction = () => {
    //действия, которые могут быть исполнены в потоке просто так, 
    //например циклы с расчетами, загрузками и прочим
    Thread.Sleep(10000); //мы же для примера просто усыпим наш тред на 10 секунд
    //если нужно выполнить какой то код из юнити апи, например создать кубик   
    GameObject cube = null;
    customComponent.Execute(() => { cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
        testThread.Interrupt(); //данное действие очень важно в конце всей последовательности
    });
    //действия, которые могут быть исполнены в потоке просто так, в cube кстати будет лежать наш кубик
}

testThread = new Thread(new ThreadStart(threadAction));
testThread.Start();

Как работает идея

Да очень просто. Что делает компонент? Он хранит очередь действий, которые нужно сделать в основном потоке. Что делает метод Execute? Он вызывается из побочного треда, и складывает в очередь исполнения действие и усыпляет поток. В самом действии в конце, есть код, который разбудит тред (та самая важная строчка). Таким образом из треда вы скармливаете небезопасный для вызова код главному треду и ждете его исполнения.

Идеи для развития

все они реализованы по сути, тут же я их просто опишу
  1. Разделение очередей. Вывести отдельные очереди для Update, LateUpdate и FixedUpdate.
  2. Автоматизация вызова Interrupt() для действий, дабы не приходилось скармливать его самому действию.
  3. Добавить возможность выполнения ассинхронных (для вызывающего потока, а не для главного) действий с колбеком.
  4. Добавить контроль времени, на случай если заданий в очереди много, как то динамически останавливать обработку и откладывать до следующего вызова.
За сим все.
`
ОЖИДАНИЕ РЕКЛАМЫ...

Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
0
29
9 лет назад
0
MF:
alexprey, до сетей я не добрался, но думаю там все можно делать по тому же принципу. Главное отдать что-то исполнять мэйн треду в нужном месте и дождаться исполнения (получить результат) или шпарить дальше, если результат не важен, а важен сам факт, что когда-то это исполнится.
Там просто прослушка порта выполняется в отдельном потоке и обработка сообщений тоже. Раньше я как делал, складывал сообщения в очередь и уже в основном потоке обрабатывал. Сейчас вся обработка идет в отдельном потоке и при необходимости что-то вызываю из основного потока. Пока что у меня это просто загрузка уровня.
А вообще да, можно было бы развить эту идею до целого крутого скрипта!
0
23
9 лет назад
0
alexprey, идея была взята со стандартного C#. Там есть такая штука, как Dispatcher. У каждого треда свой, и мы собственно нужному диспатчеру выдаем задание и ждем пока он его выполнит. Внутри все сделано было через очередь эвентов, насколько я понял. Да и подобные реализации на сторе уже есть, просто денег стоит, а формально там работы на три скрипта.
0
29
9 лет назад
Отредактирован alexprey
0
Почему не потокобезопасно. Так быстрее, не тратиться время на блокировки, а так же разработка кода как юзером так и самим разработчиками движка намного проще и быстрее проходит.
Не особо понятно зачем юзать Interrupt
А все, понял, но мне как-то не особо нравится такая концепция, надо тут подумать
0
23
9 лет назад
Отредактирован MF
0
alexprey, синхронность. Ты в один поток слипишь. В мэйн потоке его будишь. Интеррапт не прерывает поток, если он не ожидает чего-то (WaitSleepJoin состояние), если же чего-то ждет, то будет эксепшн, насколько я понимаю, и поток пойдет дальше, если эксепшн обработать. Так по сути ты обеспечиваешь не просто вызов функции в другом потоке, но и гарантируешь, что поток твой не пойдет дальше, пока функция не будет выполнена.
upd: такая концепция самая верная в том плане, что система не обрабатывает потоки, которые чего-то ждут. По сути у меня в итоговой реализации в очередь складывается не экшн, а кастомный класс, в котором хранится само действие, колбэк, поток, который вызвал все это великолепие, и функционал для вызова действия и колбэка. Так вот все слипы и интерапты у меня спрятаны и не торчат из щелей. Тут же я просто рассказывал саму концецию =)
0
30
9 лет назад
0
MF, я чет дико туплю. Статья о том, как из побочного потока передать информацию в главный?
Если да, то я всё понял, довольно годно. Делал нечто подобное =)
0
23
9 лет назад
0
Hellfim, ну по сути да, как синхронно для побочного потока выполнять код в главном.
0
29
9 лет назад
0
кст, использовать lock для листа не лучшая идея, особенно если ты используешь очередь. Лучше использовать нечто вроде LinkedList, это очень удобно, потому что процесс добавления и удаления узла потребует блокировки двух разных узлов. Плюс блокировка во время выполнения не лучший способ...
2
29
9 лет назад
2
Че неужели стандартного метода решения?
В том же джейманки модифицировать то объекты из других потоков можно сколько угодно, только в какой-то рандомный момент игра упадет с ошибкой, поэтому можно насоздавать сколько угодно своих потоков, а из них делать application.enqueue(Callable) и в callable экшны, собсно то же самое что и здесь, просто уже продумано.
0
29
9 лет назад
0
Doc, изменять значения то можно, но не создавать/уничтожать игровые объекты
0
29
9 лет назад
0
Менять в любом случае не очень практика. Там то же самое, мол вылетает эксцепшн be sure you not modifying scene graph from another thread.
2
23
9 лет назад
Отредактирован MF
2
alexprey, рекомендую почитать о потокобезопасности в c#. Лок с листами - дело нужное, иногда даже очень. Обычно только удаление и добавление локируют, но в моем случае, мне нужно чтоб очередь была гарантировано уменьшаема за кадр, мало ли кто и с какой очередностью там будет писать в очередь? По хорошему, объект sync еще и приватным надо сделать, чтоб никто иной до него не достукивался. Насчет вариаций листа: первое, это дело вкуса, второе, это производительность. Лучше всего юзать вообще массивы и выигрывать миллисекунды (именно из них складывается FPS), чем обеспечивать себе удобный кодинг. Так как тут мы имеем дело с постоянно динамическими вещами, то лист, причем дженерик, дабы не тратить время на кастование.
Doc, нет, треды в юнити вообще не юзались до пятой версии. Все однопоточно. Идеалогия движка такая. Сейчас они узкие места распаралелили, получили прирост, но для нас это все равно однопоточный пайплайн. У них есть варианты как исполнять тяжелый код - коротины. Некое распределенное между кадрами исполнение, посредством yeld return и управляющих инструкций к нему, все при этом остается в главном потоке. Это совсем не то же самое, что треды, думаю пояснять не надо. Насчет вызова и изменений... там все странно. Стандартные методы залочены почти все. НО! Никто не мешает делать свои поля и свои методы компонентам. Их можно юзать, если они не юзают внутренние. Пример: вам нужно, чтоб объект менял свои координаты. По сути вам не важно, что это произойдет реально только перед отрисовкой кадра, или перед обсчетом физики, правда? Вы делаете некое расширение стандартного класа, в котором делаете свои координаты. Их вы менять можете из тредов хоть заменятся, а уже в апдейте каком-либо эти координаты сравнивать с трансформом и накатывать их в случае необходимости. Используя этот подход и идеи изложенные выше, параллельность исполнения кода можно сделать достаточно гибкой и устойчивой к падениям. В любом случае - это вопрос целеполагания.
Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.