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

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

А вся суть в том, что 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
Кст, при работе с сетью тоже возникла такая проблема, реализовал компонент с очередью, туда складывал экшены, которые требуется запустить в основном потоке. И основной компонент который в FixedUpdate вызывал это действие)
Подробнее еще не прочитал пока что... ((
0
23
9 лет назад
0
alexprey, до сетей я не добрался, но думаю там все можно делать по тому же принципу. Главное отдать что-то исполнять мэйн треду в нужном месте и дождаться исполнения (получить результат) или шпарить дальше, если результат не важен, а важен сам факт, что когда-то это исполнится.
0
27
9 лет назад
0
Очень хорошее решение серьезной проблемы
А еще мне очень нравится как MF пишет. Просто, ясно, доступно
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 и управляющих инструкций к нему, все при этом остается в главном потоке. Это совсем не то же самое, что треды, думаю пояснять не надо. Насчет вызова и изменений... там все странно. Стандартные методы залочены почти все. НО! Никто не мешает делать свои поля и свои методы компонентам. Их можно юзать, если они не юзают внутренние. Пример: вам нужно, чтоб объект менял свои координаты. По сути вам не важно, что это произойдет реально только перед отрисовкой кадра, или перед обсчетом физики, правда? Вы делаете некое расширение стандартного класа, в котором делаете свои координаты. Их вы менять можете из тредов хоть заменятся, а уже в апдейте каком-либо эти координаты сравнивать с трансформом и накатывать их в случае необходимости. Используя этот подход и идеи изложенные выше, параллельность исполнения кода можно сделать достаточно гибкой и устойчивой к падениям. В любом случае - это вопрос целеполагания.
0
29
9 лет назад
0
MF, я знаю про потокобезопасность. линкед лист, это не массив, а просто узлы с которыми в данной задаче можно работать за O(1). И он не нуждается весь в блокировке в отличии от листа.
MF:
Пример: вам нужно, чтоб объект менял свои координаты. По сути вам не важно, что это произойдет реально только перед отрисовкой кадра, или перед обсчетом физики, правда? Вы делаете некое расширение стандартного класа, в котором делаете свои координаты. Их вы менять можете из тредов хоть заменятся, а уже в апдейте каком-либо эти координаты сравнивать с трансформом и накатывать их в случае необходимости. Используя этот подход и идеи изложенные выше, параллельность исполнения кода можно сделать достаточно гибкой и устойчивой к падениям. В любом случае - это вопрос целеполагания.
Разумное решение с использованием внешней памяти, все так и работает, даже физ движок так и построен ему передаются данные перед расчетом, параллелится и потом забираются обратно
0
23
9 лет назад
Отредактирован MF
0
alexprey, ты не понял видимо для чего я блокирую все чтение листа, это раз, ты не понял что я имел ввиду под скоростью (не путать со сложностью алгоритма, что ты и привел), это два.
UPD: По результатам теста, заполнение LinkedList в два раза медленнее, чем обычного листа.
0
29
9 лет назад
0
MF, нет не понял. Просто при блокировке всего листа, нельзя будет добавить новый узел, пока заблокирован начальный
0
23
9 лет назад
0
alexprey, в данной задаче это и нужно. Очистка листа проводится между кадрами, то есть нужно быть уверенным в том, что за это время лист не разрастется. Я его для этого и блокирую, треды ждут очистки, все счастливы. =)
0
29
9 лет назад
0
треды ждут очистки, все счастливы. =)
треды ждут
треды не счастливы :D
0
23
9 лет назад
0
alexprey, главное, чтоб был счастлив мэйн тред. =) Треды с отложенным исполнением в любом случае будут ждать. К тому же ждут они только в случае именно этого самого отложенного исполнения.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.