Самый оптимальный дизайн потока данных TPL?

Мне нравится спрашивать, как лучше всего спроектировать наиболее оптимальную архитектуру с использованием TPL Dataflow. Я еще не написал кода, поэтому я не могу опубликовать образец кода. Я тоже не ищу код (если не вызван добровольно), но помощь в дизайне будет очень благодарна:

Требования следующие:

У меня есть 3 основных блока данных, которые определенным образом зависят друг от друга. Datablock1 - производитель, который создает объекты типа Foo1. Предполагается, что Datablock2 подписывается на объекты Foo1 (из Datablock1) и потенциально (не на каждый и каждый Foo1, в зависимости от конкретной функции) создавать объекты Foo2, которые он сохраняет в очереди вывода для использования другими блоками данных. Datablock3 также потребляет объекты Foo1 (из Datablock1) и потенциально создает объекты Foo3, которые потребляет Datablock2 и преобразует в объекты Foo2.

Итак, вот блоки данных и то, что каждый из них производит и потребляет:

  • Datablock1: производит (Foo1), потребляет (ничего)
  • Datablock2: производит (Foo2), потребляет (Foo1, Foo3)
  • Datablock3: производит (Foo3), потребляет (Foo1)

Дополнительным требованием является то, что один и тот же Foo1 обрабатывается примерно в одно и то же время в Datablock2 и Datablock3. Было бы нормально, если бы объекты Foo1 сначала использовались Datablock2, а затем, как только Datablock2 выполнил свою работу, те же самые объекты Foo1 были отправлены в Datablock3, чтобы он выполнял свою работу. Объекты Foo2 из Datablock2 могут быть результатом операций с объектами Foo1 или объектами Foo3.

Я надеюсь, что в этом есть смысл, я буду рад объяснить больше, если это все еще неясно.

Моей первой идеей было создать блоки потока данных TPL для каждого из трех блоков данных и заставить их обрабатывать входящие потоки различных типов объектов. Другая идея - разделить блоки данных и сделать так, чтобы каждый блок данных обрабатывал потоки только одного типа объекта. Что вы порекомендуете или есть еще лучшее решение, которое может сработать?

Свик уже помог с Datablock1, и он уже работает, я просто застрял в том, как преобразовать мою текущую среду (как описано выше) в поток данных TPL.

Мы очень ценим любые идеи или указатели.


person Matt    schedule 28.06.2012    source источник
comment
Если вы ищете наиболее эффективную архитектуру, есть только один способ правильно ее найти: измерить различные варианты, чтобы выяснить, какой из них на самом деле лучший. Обо всем остальном будут только догадки.   -  person svick    schedule 28.06.2012
comment
Полностью согласен, но на данный момент я не уверен, можно ли настроить блок данных для приема объектов из разных связанных с ним ISourceBlocks. Если бы я знал, как написать блок данных, который может принимать, например, объекты Foo1 и Foo2, буферизовать их в 2 отдельных входных очередях и работать с переданной функцией, то я мог бы сам настроить некоторую архитектуру тестирования. Проблема в том, что документации TPL Dataflow в настоящее время практически не существует. Не могли бы вы помочь с принятием решения о передаче нескольких производителей разными объектами в один блок потока данных?   -  person Matt    schedule 28.06.2012
comment
Есть ли у Foo1 и Foo3 общий базовый класс? Если нет, то почему блок 2 может обрабатывать и то, и другое?   -  person svick    schedule 28.06.2012
comment
Они не разделяют ни один базовый класс. Дело в том, чтобы выяснить, можно ли спроектировать блок, который принимает / обрабатывает два разных потока (объектов разного типа). В этом весь смысл этого упражнения - выяснить, что можно сделать, а что нет, потому что в документации TPL Dataflow нечего сказать, по крайней мере, я не нашел многого. Я наткнулся на BatchedJoinBlock, но не уверен, применим ли он здесь ...   -  person Matt    schedule 28.06.2012
comment
@svick, мой вопрос заключался в том, можно ли разработать блок потока данных для приема потоков разных типов из двух отдельных блоков данных. Я думаю, это звучит как разумное требование, чтобы TPL Dataflow предоставлял тот или иной способ, но дело в том, что документация настолько скудна, что я до сих пор не видел ничего, что упоминало бы какой-либо тип блока, который может обрабатывать несколько типов данных самостоятельно. соответствующие очереди ввода.   -  person Matt    schedule 29.06.2012


Ответы (1)


Разобьем эту задачу на три и решим каждую отдельно.

Первый - как изготовить предмет условно. Я думаю, что лучший вариант - использовать _1 _ и пусть ваша функция вернет коллекцию с одним или нулевым элементом.

Другой вариант - условно связать два блока, так что nulls игнорируются и возвращают null, когда вы не хотите ничего производить. Но если вы это сделаете, вам также необходимо связать источник с _ 4_, чтобы nulls не оставались в его выходном буфере.

Вторая проблема заключается в том, как отправить Foo1 как в блок №2, так и в блок №3. Здесь я вижу два пути:

  1. Используйте BroadcastBlock, связанный с обоими целевыми блоками (№2 и №3). Будьте осторожны с этим, потому что BroadcastBlock не имеет очереди вывода, поэтому, если целевой блок откладывает элемент, это означает, что он не будет его обрабатывать. По этой причине в этом случае не следует устанавливать BoundedCapacity блоков №2 и №3. Если вы этого не сделаете, они никогда не будут откладывать, и все сообщения будут обрабатываться обоими блоками.
  2. После обработки Foo1 блоком №2 вручную Post() (или лучше SendAsync()) его в блок №3.

Я не уверен, что именно означает «примерно в одно и то же время», но в целом TPL Dataflow не дает никаких гарантий относительно порядка обработки независимых блоков. Вы можете изменить приоритет различных блоков, используя пользовательский TaskScheduler < / a>, но я не уверен, что это было бы здесь полезно.

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

  1. Не обрабатывайте их одним блоком. Возьмите один TransformBlock<Foo1, Foo2> и один TransformBlock<Foo3, Foo2>. Затем вы можете связать их оба в один BufferBlock<Foo2>.
  2. Как вы предложили, используйте BatchedJoinBlock<Foo1, Foo3> с batchSize из 1. Это означает, что результирующий Tuple<IList<Foo1>, IList<Foo3>> всегда будет содержать либо один Foo1, либо один Foo3.
  3. Усовершенствуйте предыдущее решение, связав BatchedJoinBlock с TransformBlock, что даст более подходящий тип. Это может быть либо Tuple<Foo1, Foo3> (один из элементов всегда будет null), либо что-то вроде F # Choice<Foo1, Foo3>, что гарантирует, что будет установлен только один из двух.
  4. Создайте новый тип блока с нуля, который сделает именно то, что вы хотите. Он должен быть ISourceBlock<Foo2> и иметь два свойства: Target1 типа ITarget<Foo1> и Target2 типа ITarget<Foo3>, как встроенные блоки соединения.

С вариантами № 1 и № 3 вы также можете инкапсулировать блоки в один настраиваемый блок, который снаружи выглядит как блок № 4, чтобы его было легче использовать повторно.

person svick    schedule 29.06.2012
comment
Могу я пояснить, что вы имеете в виду под условным производством? Это необходимо, потому что TransformBlocks обычно помещает в очередь вывода ровно столько же элементов, сколько поступает во входную очередь? Будет ли использование Actionblock и Posting при выполнении условия грязным решением? - person Matt; 29.06.2012
comment
Примерно в то же время я хотел указать на тот факт, что файлы Foo3, создаваемые DataBlock3 и потребляемые DataBlock2, зависят от состояний определенных переменных, которые установлены в Datablock2 с помощью Foo1. Я думаю, что единственный способ обойти эту проблему - объединить блоки данных 2 и 3. Или использовать разные блоки данных для каждого типа потока. - person Matt; 29.06.2012
comment
Да, условное производство означает, что только некоторые входы дают результат. И TransformBlock производит столько же товаров, сколько потребляет, да. В целом, я думаю, что ссылки предпочтительнее публикации, но в этом случае я думаю, что это допустимый вариант. - person svick; 29.06.2012
comment
А зависимость одного блока от некоторых внешних данных противоречит идее потока данных TPL (и модели акторов, на которой он построен). Если какому-то блоку нужно что-то особенное для предмета, он должен идти вместе с этим предметом. Из-за этого ручной Post(), вероятно, здесь лучший вариант, он обеспечит правильный порядок операций. - person svick; 29.06.2012
comment
Я выбрал вариант №1, он дает более чистый код и работает достаточно быстро. Однако есть один вопрос: есть ли способ в Actionblock ожидать обработки следующего элемента в очереди, пока текущий элемент не будет полностью обработан не только внутри самого блока действий, но и если элемент был передан через Post (элемент) в другой блок данных. Есть ли какой-то механизм, чтобы сообщить, что текущий элемент полностью обработан, и дать команду Actionblock обработать следующий элемент в очереди? - person Matt; 02.07.2012
comment
Вся идея TPL Dataflow заключается в том, что обработка в каждом блоке независима. Если вы хотите дождаться завершения некоторой обработки элемента, я думаю, что эта часть не должна быть отдельным блоком. Вы можете превратить его в обычный метод, который вы вызываете из ActionBlock. - person svick; 02.07.2012
comment
спасибо за то, что поделились своим обширным опытом работы с TPL Dataflow. Извините, я не обученный программист и поэтому не знаю, входите ли вы в команду разработчиков MS TPL Dataflow? - person Matt; 03.07.2012
comment
Я польщен, что вы так думаете, но я не имею отношения к Microsoft или команде Dataflow. - person svick; 03.07.2012
comment
Я пометил ваш ответ как желаемое решение, потому что он меня вдохновил, и в конце концов я решил реализовать отдельные блоки данных, каждый из которых обрабатывает один характерный тип var. Я считаю, что это лучший компромисс между чистой скоростью, чистотой кода и элегантностью. Иногда мне приходится использовать Post / SendAsync вместо того, чтобы связывать блоки данных, но я думаю, что в настоящее время я не смогу легко обойти это при выводе элементов с разной частотой, с которой они поступают. Еще раз спасибо за ваше время и усилия, чтобы помочь с этим . - person Matt; 03.07.2012
comment
Я только что придумал способ получить 0 или 1 элемент из TransformBlock: написать transform лямбда, которая возвращает Task.FromResult(value), чтобы вернуть 1 элемент, и return null Task, чтобы вернуть 0 элементов. Использование TransformManyBlock, вероятно, более понятно, но это также может быть интересным вариантом. - person svick; 25.07.2012