Страница 1 из 1

Пускаем волны многопоточности[Job System] Часть 2.

СообщениеДобавлено: 30 май 2018, 21:23
lawsonilka
Пускаем волны многопоточности. Часть 2.

предыдущая часть

При работе с многопоточными вычислениями всегда встает острый вопрос о необходимости синхронизации данных.

...данные в потоках.
Скрытый текст:
Синхронизацию можно представить себе так. Вы приходите в банк чтобы обменять евро на доллары. Вы стоите возле окошка оператора, ждете когда очередь доберется до вас, наконец передаете оператору определенную сумму евро, оператор идет в хранилище и берет эквивалентную сумму долларов, возвращается и отдает вам их в окошке. Так работает один поток - он сам запрашивает и изменяет данные в хранилище. Но это долго, особенно в больших очередях, пока ОДИН оператор обслужить всех посетителей может уйти много времени. Теперь представьте что мы имеем несколько окошек с несколькими операторами в банке, теперь работа идет быстрей, все больше людей меняют евро на доллары. Вы также подходите к оператору, отдаете ему сумму евро, он идет в хранилище, но возвращается он ни с чем так как в хранилище закончились доллары. Здесь возникает конфликт когда вы запросили данные в хранилище, но их там нет, или они могут быть изменены и очевидно что такая работа вам не понравиться - вы отдали деньги, а результата нет.


Для работы с данными в потоках юнитеки на низком уровне API предоставили контейнеры NativeContainer в которые складываются эти данные. Для обеспечения безопасности они выполнены в виде структур. Контейнеры поддерживают только самые простые типы данных, а также самое важно - не преобразуемыми типы данных, то есть тот же boolean или string вы поместить в них уже не сможете.

Эти контейнеры обеспечивают синхронизацию данных между потоками. Сейчас доступны только NativeArray и NativeSlice контейнеры. В первом случае это массив который вы можете наполнить элементарными данными, во втором случае вспомогающий контейнер для "нарезки" NativeArray на части. В будущем будут введены еще NativeList, NativeHashMap, и NativeQueue для работы с очередями. Также можно создавать свои контейнеры, но вам придется все тщательно протестировать чтобы не возникло проблем. В любом случае для обмена элементарными данными хватит пока и NativeArray.

...почему так!?
Скрытый текст:
Лично мое предположение, что эти контейнеры сериализуются внутри уже через JSON, судя по работе с transform'ом так можно получить независимую копию данных.


В работе все выглядит просто: наполняем контейнер данными, отправляем его на выполнение, после завершения принимаем данные из контейнера обратно.
Примечание! Так как эти контейнеры представляют собой неконтроллируемый ресурс, после выполнения работы необходимо высвобождать их из памяти.

Давайте создадим простой контейнер для начала. Не забывайте импортировать нужные библиотеки!
Синтаксис:
Используется csharp
using Unity.Collections;
using Unity.Jobs;

public class SomeBehaviour : MonoBehaviour {

 void Start() {
  NativeArray<float> array;

  array.Dispose();//Высвобождаем занятые ресурсы.
 }

}


Для начала мы указали в скобочках каким типом(float) данных мы будем наполнять контейнер, теперь создадим сам экземпляр.
Синтаксис:
Используется csharp
public class SomeBehaviour : MonoBehaviour {

 void Start() {
  NativeArray<float> array = new NativeArray<float>(100, Allocator.Temp);

  array.Dispose();//Высвобождаем занятые ресурсы.
 }

}

При создании экземпляра нужно указать в его конструкторе несколько параметров:
- размер массива
- тип хранения данных.

Размер массива можно задать каким угодно, а вот тип хранения данных указывает на то как быстро поток сможет воспользоваться данными из контейнера, от этого будет и зависеть скорость выполнения задачи. Есть несколько типов хранения данных:
-Temp. Обеспечивает самый быстрый тип доступа, но тем не менее не слишком потокобезопасен. Рекомендуется для выполнения быстрых задач с минимумом данных.
-TempJob. Обеспечивает доступ к данным чуть медленнее чем тип Temp, рекомендуется для выполнения коротких и сложных задач.
-Persistance. Хранит данные разрозненно, потоку нужно больше времени чтобы собрать эти данные, рекомендуется для задач с большим объемом данных.
К примеру если зададите тип Temp с большим объемом данных - могут случаться провисания, напротив задатите тип Persistance с легкой задачей и она может выполняться неестественно долго.

В примере указан тип Temp, так что задача сможет быстро выполнить чтение и запись данных, буквально за один кадр.

Теперь создадим структуру задачи унаследованную от интерфейса IJobParallelFor.
Синтаксис:
Используется csharp
public struct ArrayJob : IJobParallelFor {

 public void Execute(int index) {
 
 }

}

Как вы помните структура это тип данных, и при передаче они копируют себя, так что для передачи контейнера в задачу нам нужно просто скопировать массив в экземпляр структуры.
Синтаксис:
Используется csharp
public struct ArrayJob : IJobParallelFor {

 public NativeArray<float> inputArray;

 public void Execute(int index) {
 
 }

}


Теперь в методе Start передадим созданный контейнер в структуру.
Синтаксис:
Используется csharp
public class SomeBehaviour : MonoBehaviour {

 void Start() {
  NativeArray<float> array = new NativeArray<float>(100, Allocator.Temp);
  ArrayJob job = new ArrayJob() {inputArray = array};//тоже самое что и job.inputArray = array;
 }

}

Теперь создадим JobHandle задачи.
Синтаксис:
Используется csharp
public class SomeBehaviour : MonoBehaviour {

 void Start() {
  NativeArray<float> array = new NativeArray<float>(100, Allocator.Temp);
  ArrayJob job = new ArrayJob() {inputArray = array};//тоже самое что и job.inputArray = array;
  JobHandle handle = job.Schedule(array.Length, 10);

  array.Dispose();//Высвобождаем занятые ресурсы.
 }

}

Для запуска задачи в IJobParallel необходим новый метод Schedule который принимает два числовых параметра:
-число повторений задачи.
-размер блоков для выполнения.

Число повторений просто указывает сколько раз выполниться метод Execute в задаче.
Размер блоков нужен уже конкретно для планировщика задач. Дело в том что раньше когда мы запускали задачу в IJob, там она выполнялась только один раз в одном потоке, сейчас же мы хотим чтобы задача выполнялась много раз, для этого планировщик распределяет выполнение этих повторений на разные потоки, а размеры блоков указывают на сколько частей можно поделить и распределить между потоками выполнение этих задач.
К примеру если у вас размер массива 100 и вы укажите размер блоков как 100, то планировщик разложит сотню повторений на сотню блоков, то есть любой поток сможет выполнить только один(1) раз задачу. Это не совсем правильный подход, ведь чем больше будет блоков, тем больше потребуется потоков для выполнения задачи, если блоков наоборот будет слишком мало, а задача слишком сложная то могут возникнут провисания. Здесь нужно искать компромисс между сложностью задачи, и скоростью ее выполнения.

Теперь можно запустить задачу, но прежде нужно указать в методе Execute что мы хотим выполнить с данными массива. К примеру будем умножать индекс на самого себя каждый раз.
Синтаксис:
Используется csharp
public struct ArrayJob : IJobParallelFor {

 public NativeArray<float> inputArray;

 public void Execute(int index) {
  float value = index * index;
  this.inputArray[index] = value;
 }

}

Как видите, сначала мы провели нужные вычисления и потом записали полученный результат в контейнер под определенным индексом.

Далее вызываем метод Complete который выполнит задачу.
Синтаксис:
Используется csharp
public class SomeBehaviour : MonoBehaviour {

 void Start() {
  NativeArray<float> array = new NativeArray<float>(100, Allocator.Temp);
  ArrayJob job = new ArrayJob() {inputArray = array};//тоже самое что и job.inputArray = array;
  JobHandle handle = job.Schedule(array.Length, 10);

  handle.Complete();

  array.Dispose();//Высвобождаем занятые ресурсы.
 }

}


Задача выполниться практически мгновенно, так что можно будет получить результат сразу же в методе Start.
Выведем результат первой(не нулевой, так как ноль умножить на ноль будет ноль) ячейки массива.
Синтаксис:
Используется csharp
public class SomeBehaviour : MonoBehaviour {

 void Start() {
  NativeArray<float> array = new NativeArray<float>(100, Allocator.Temp);
  ArrayJob job = new ArrayJob() {inputArray = array};//тоже самое что и job.inputArray = array;
  JobHandle handle = job.Schedule(array.Length, 10);

  handle.Complete();

  float result = job.inputArray[1];
  print("Result: " + result);

  array.Dispose();//Высвобождаем занятые ресурсы.
 }

}

Если вы проделали все также как и в примере, то у вас будет результат в консоли 1.

Так как контейнеры представляют из себя что то вроде "моста" данных между потоками, то при изменении этих данных в любом потоке результаты также будут видны и в других, конечно же только после выполнения задачи.

...так это же структуры
Скрытый текст:
Движок синхронизирует данные в структурах так что и оригинал и копия изменяются вместе, то есть в примере выше, можно также обратиться к экземпляру array[1] и получить 1 как мы сделали это через обращения к задаче и ее массиву job.inputArray[1]. Оба эти экземпляра(array и job.inputArray) ссылают на одни и те же данные. Это касается только контейнеров.


Теперь вы знаете как работать с данными в потоках чтобы не возникало конфликтов, как их передавать, изменять и принимать обратно результат работы. А также как распределять выполнение одной задачи между несколькими потоками.

В следующей части я расскажу на примере как же все таки использовать эту многопоточность с объектами в unity.

Следующая часть...

Re: Пускаем волны многопоточности[Job System] Часть 2.

СообщениеДобавлено: 01 июн 2018, 22:47
seaman
Ну уж точно не в JSON контейнеры сериализуются.
Я думаю что вся реализация этой джобСистем сделана на плюсах. А для передачи данных туда из шарпа нужен маршаллинг данных. Оттого и говорится о не преобразуемых объектах. Маршаллинг можно было бы сделать почти для чего угодно, но проще для заранее заданных структур - NativeContainer.

Re: Пускаем волны многопоточности[Job System] Часть 2.

СообщениеДобавлено: 01 июн 2018, 23:29
lawsonilka
Я думаю что вся реализация этой джобСистем сделана на плюсах.

реализация это да так и есть
А для передачи данных туда из шарпа нужен маршаллинг данных

да это все тоже обсуждалось в одном из видео презентаций.
Маршаллинг можно было бы сделать почти для чего угодно, но проще для заранее заданных структур - NativeContainer.

То есть теоретически возможно передать более сложные объекты? - хотя раз юнитеки уже подготовили List и Queue контейнеры, то скорей всего так они все и оставят взаимодействовать через них.

Re: Пускаем волны многопоточности[Job System] Часть 2.

СообщениеДобавлено: 01 мар 2019, 01:40
AlexandrBuryakov
А можно NativeArray как то использовать постоянно, не убивая.
То есть выполнилось в кадре, данные изменились, мы их скопировали во внешний массив, но контейнер оставили, что бы эти изменённые данные потом опять в этой же задаче обработать??? А то каждый раз копировать то, что выгружаешь с предыдущего раза, как то бессмысленно.

Re: Пускаем волны многопоточности[Job System] Часть 2.

СообщениеДобавлено: 01 мар 2019, 15:31
lawson
Да для поддержания максимального времени жизни структуры NativeArray необходимо указать тип создания Persistente, но не забудьте выгрузить ее после использования.

Re: Пускаем волны многопоточности[Job System] Часть 2.

СообщениеДобавлено: 01 мар 2019, 16:51
1max1
Жаль под вебгл не распараллелишь)

Re: Пускаем волны многопоточности[Job System] Часть 2.

СообщениеДобавлено: 01 мар 2019, 20:53
AlexandrBuryakov
NativeArray в варианте с Persistente на сколько я понял имеет самую низкую производительность, по сравнению с другими двумя?
А то возникло впечатление, что Temp самый быстрый и не очень безопасный и ограничен временем в один кадр. (Чем не безопасный в многопоточке?)
TempJob более безопасный для многопоточки, но по медленней и ограничен временем в 4 кадра, то есть даже если задача мелкая, может тормознуть (отложить) выполнение, если всё забито Temp вариантами рассчитанными на 1 кадр.
Persistente вообще по памяти разбросан (Не понял, что это означает в описании???) и имеет наименьшую производительность из этой тройки и не ограничен временем существования. Не пойму, почему нельзя сделать (хотя бы вариант) без ограничений , безопасно и на постоянной основе вариант цикла, который имеет самый высокий приоритет (для функционирования игры) работы. Самому просто писать не охота, я там чокнусь с многопоточностью.
Однако в принципе рассчитать, что какой то важный мне массив не будет потревожен, пока обрабатывается, так это же просто... что за шаманство с кучей копирований?
Не вижу проблем в том, что бы оригинальный массив был многопоточно обработан (одновременно несколько кусков) и только после этого использованы данные, благодаря команде синхронизации (ожидание завершение всех потоков по этому массиву).... да даже не вижу смысла запрещать чтение массива пока он обрабатывается (хотя бы атрибут для разрешения чтения при выполнении), в некоторых случаях, залезть и считать данные независимо от стадии отработки вполне нормально, это иногда не имеет разницы. В следующий раз (при чтении) будут данные уже более обновлённые и что? Ну на 1 кадр отобразится что-то чуть позже... от этого ничего не теряется, никто не заметит, зато ожидать не надо и всех тормозить в логике системы.
Что думаете?

Re: Пускаем волны многопоточности[Job System] Часть 2.

СообщениеДобавлено: 02 мар 2019, 06:07
lawson
Что думаете?

Ну вообщето обрабатывать один массив в нескольких потоках можно, для этого и есть IJobParralelFor

Ладно попробуйте почитать чуть более расширенный вариант этой статьи на хабре

Re: Пускаем волны многопоточности[Job System] Часть 2.

СообщениеДобавлено: 02 мар 2019, 20:35
AlexandrBuryakov
lawson писал(а):
Что думаете?

Ну вообщето обрабатывать один массив в нескольких потоках можно, для этого и есть IJobParralelFor

Ладно попробуйте почитать чуть более расширенный вариант этой статьи на хабре


Я про другое, про то, что не очень понятно, как обойтись без копирований в каждый поток (NativeArray) перед началом обработки и потом после обработки копирование из NativeArray в основной массив. Как сделать работу с массивами без кучи бессмысленных перекопирований?

Re: Пускаем волны многопоточности[Job System] Часть 2.

СообщениеДобавлено: 03 мар 2019, 15:22
lawson
А зачем вам копировать NativeArray в каждый поток?
Может у вас просто логика задачи неправильная?