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

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

А вся суть в том, что 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
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
Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.