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

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

Научился сам? Помоги начинающему.

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

Сообщение lawsonilka 30 май 2018, 21:23

Пускаем волны многопоточности. Часть 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.

Следующая часть...
Последний раз редактировалось lawsonilka 12 июн 2018, 19:38, всего редактировалось 1 раз.
lawsonilka
UNIверсал
 
Сообщения: 380
Зарегистрирован: 21 окт 2014, 14:48

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

Сообщение seaman 01 июн 2018, 22:47

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

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

Сообщение lawsonilka 01 июн 2018, 23:29

Я думаю что вся реализация этой джобСистем сделана на плюсах.

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

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

То есть теоретически возможно передать более сложные объекты? - хотя раз юнитеки уже подготовили List и Queue контейнеры, то скорей всего так они все и оставят взаимодействовать через них.
lawsonilka
UNIверсал
 
Сообщения: 380
Зарегистрирован: 21 окт 2014, 14:48


Вернуться в Уроки

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 2