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

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

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