TPL Dataflow: дизайн для параллелизма при сохранении порядка

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

  1. нарисуйте несколько текстовых / растровых изображений на рамке
  2. обрезать кадр
  3. изменить размер кадра
  4. уменьшить изображение до 256 цветов

Очевидно, что этот процесс можно выполнять параллельно для всех кадров в списке, но для каждого кадра порядок шагов должен быть одинаковым. После этого мне нужно записать все кадры в файл gif. Поэтому все кадры должны быть получены в том же порядке, в котором они были в исходном списке. Кроме того, этот процесс может начаться, когда для него будет готов первый кадр, нет необходимости ждать, пока будут обработаны все кадры.

Вот такая ситуация. Подходит ли для этого TPL Dataflow? Если да, может ли кто-нибудь дать мне подсказку в правильном направлении о том, как спроектировать структуру блока tpl, чтобы отразить процесс, описанный выше? Это кажется мне довольно сложным по сравнению с некоторыми образцами, которые я нашел.


person p4gefault    schedule 03.02.2014    source источник


Ответы (3)


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

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

public Task CreateAnimationFileAsync(IEnumerable<Bitmap> frames)
{
    var frameProcessor = new TransformBlock<Bitmap, Bitmap>(
        frame => ProcessFrame(frame),
        new ExecutionDataflowBlockOptions
        { MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded });

    var animationWriter = new ActionBlock<Bitmap>(frame => WriteFrame(frame));

    frameProcessor.LinkTo(
        animationWriter,
        new DataflowLinkOptions { PropagateCompletion = true });

    foreach (var frame in frames)
    {
        frameProcessor.Post(frame);
    }

    frameProcessor.Complete();

    return animationWriter.Completion;
}

private Bitmap ProcessFrame(Bitmap frame)
{
    …
}

private async Task WriteFrame(Bitmap frame)
{
    …
}
person svick    schedule 17.02.2014
comment
Преимущество использования нескольких блоков заключается в том, что вы можете легко изменить их, чтобы обработать их по-другому. - person Matt Carkci; 19.02.2014
comment
@MattCarkci Да, в некоторых случаях это может быть полезно. Но здесь, я думаю, изменить ProcessFrame() так же просто, как изменить блоки, и это приводит к более простому коду. - person svick; 19.02.2014
comment
Вы должны вернуть анимациюWriter.opletion, а не блок преобразования. - person Poul K. Sørensen; 21.03.2016
comment
MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded немного пугает. Это означает, что параллелизм будет существенно задушен голодающим ThreadPool. Я бы предпочел MaxDegreeOfParallelism = Environment.ProcessorCount. Или, может быть, MaxDegreeOfParallelism = Environment.ProcessorCount * 2, чтобы сбалансировать объемную рабочую нагрузку за счет некоторых накладных расходов, вызванных переключением потоков. В последнем случае я бы также увеличил минимальное количество потоков, немедленно создаваемых пулом потоков: ThreadPool.SetMinThreads(Environment.ProcessorCount * 2, 10) - person Theodor Zoulias; 07.06.2020

Ваша проблема — прекрасный пример превосходства потока данных.

Вот самый простой код, с которого можно начать.

// Try increasing MaxDegreeOfParallelism
var opt = new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 2 };

// Create the blocks
// You must define the functions to do what you want
var paintBlock = new TransformBlock<Bitmap, Bitmap>(fnPaintText, opt);
var cropBlock = new TransformBlock<Bitmap, Bitmap>(fnCrop, opt);
var resizeBlock = new TransformBlock<Bitmap, Bitmap>(fnResize, opt);
var reduceBlock = new TransformBlock<Bitmap, Bitmap>(fnReduce,opt);

// Link the blocks together
paintBlock.LinkTo(cropBlock);
cropBlock.LinkTo(resizeBlock);
resizeBlock.LinkTo(reduceBlock);

// Send data to the first block
// ListOfImages contains your original frames
foreach (var img in ListOfImages) { 
   paintBlock.Post(img);
}

// Receive the modified images
var outputImages = new List<Bitmap>();
for (int i = 0; i < ListOfImages.Count; i++) {
   outputImages.Add(reduceBlock.Receive());
}

// outputImages now holds all of the frames
// reassemble them in order
person Matt Carkci    schedule 03.02.2014
comment
Это не правильно. Даже с установленным MaxDegreeOfParallelism Dataflow всегда автоматически сохраняет сообщения в правильном порядке, нет необходимости в Wrapper. - person svick; 17.02.2014

Я думаю, вы обнаружите, что DataFlow — правильный путь. Для каждого кадра из вашего списка кадров попробуйте создать один файл TransformBlock. Для каждого из четырех шагов соедините кадры в правильном порядке. Если вы хотите обрабатывать список фреймов одновременно, вы можете использовать bufferblock для списка фреймов.

пожалуйста, найдите полный пример использования transformblock на MSDN:

person devhorse    schedule 03.02.2014
comment
Я думаю, что использование одного блока на frame не имеет особого смысла. Один блок на шаг в процессе может. - person svick; 17.02.2014