为什么任务不是并行的?

时间:2017-03-04 14:23:45

标签: c# async-await tpl-dataflow

我有多个位置(称为Cell),我在那里运行测试。测试实现为异步任务并因此运行。用户可以选择为每个单元格运行任何测试。如果我选择在所有单元格上运行相同的完全相同的测试,那么它或多或少会并行。

测试A, B, C,如果在单元格1和2上我选择测试A, B,在3上我只选择C,那么由于某种原因,单元格1和2中的测试将开始运行,但在单元格3中,测试C将无法启动,直到单元格1和单元格2中的A和B测试无法完成。基本上所有单元格中的所有测试都倾向于以相同的顺序运行。那不是我想要的。我试图实现的是从每个单元独立运行的测试链。现在我将展示我的实施方式。

private async void buttonStartTest_Click(object sender, EventArgs e)
{
    var cells = objectListView.CheckedObjects.Cast<Cell>().ToList();
    if (cells == null)
        return;

    var blockPrepare = CreateExceptionCatchingTransformBlock(new Func<Cell, Task<Cell>>(Tests.Prepare), new Action<Exception, Cell>(HandleUnhandledException), new ExecutionDataflowBlockOptions
    {
        BoundedCapacity = 10000,
        MaxDegreeOfParallelism = 40,
    });

    var blockFinalize = CreateExceptionCatchingActionBlock(new Func<Cell, Task>(Tests.Finalize), new Action<Exception, Cell>(HandleUnhandledException), new ExecutionDataflowBlockOptions
    {
        BoundedCapacity = 10000,
        MaxDegreeOfParallelism = 40,
    });

    List<IPropagatorBlock<Cell, Cell>> blockList = new List<IPropagatorBlock<Cell, Cell>>();
    var funcs = tests.Select(x => x.Value);
    foreach (var func in funcs)
    {
        var blockNew = CreateExceptionCatchingTransformBlock(new Func<Cell, Task<Cell>>(func), new Action<Exception, Cell>(HandleUnhandledException), new ExecutionDataflowBlockOptions
        {
            BoundedCapacity = 10000,
            MaxDegreeOfParallelism = 40,
        });
        blockList.Add(blockNew);
    }

    // link
    for (int i = 0; i < blockList.Count - 1; i++)
    {
        var b1 = blockList[i];
        var b2 = blockList[i + 1];
        b1.LinkTo(b2);
    }

    // link first and last
    blockPrepare.LinkTo(blockList[0], new DataflowLinkOptions { PropagateCompletion = true });
    blockList[blockList.Count - 1].LinkTo(blockFinalize, new DataflowLinkOptions { PropagateCompletion = true });

    foreach (Cell c in cells)
    {
        c.Reset();
        c.State = Cell.States.InProgress;
        var progressHandler = new Progress<string>(value =>
        {
            c.Status = value;
        });

        c.Progress = progressHandler as IProgress<string>;
        blockPrepare.Post(c);
    };

    blockPrepare.Complete();
    try
    {
        await blockFinalize.Completion;
    }
    catch (Exception ex)
    {
        logger.Debug(ex.InnerException.InnerException.Message);
    }
}

上面你可以看到每个单元格的2个必需块 - 准备和完成。以下是我创建它们的方法:

public IPropagatorBlock<TInput, TOutput> CreateExceptionCatchingTransformBlock<TInput, TOutput>(
                Func<TInput, Task<TOutput>> transform,
                Action<Exception, Cell> exceptionHandler,
                ExecutionDataflowBlockOptions dataflowBlockOptions)
{
    return new TransformManyBlock<TInput, TOutput>(async input =>
    {
        try
        {
            var result = await transform(input);
            return new[] { result };
        }
        catch (Exception ex)
        {
            exceptionHandler(ex, (input as Cell));

            return Enumerable.Empty<TOutput>();
        }
    }, dataflowBlockOptions);
}

public ITargetBlock<TInput> CreateExceptionCatchingActionBlock<TInput>(
                Func<TInput, Task> action,
                Action<Exception, Cell> exceptionHandler,
                ExecutionDataflowBlockOptions dataflowBlockOptions)
{
    return new ActionBlock<TInput>(async input =>
    {
        try
        {
            await action(input);
        }
        catch (Exception ex)
        {
            exceptionHandler(ex, (input as Cell));
        }
    }, dataflowBlockOptions);
}

测试本身看起来像这样:

public static async Task<Cell> TestDoorsAsync(Cell c)
{
    int thisTestID = TEST_DOORS;
    TestConfiguration conf = c.GetConfiguration(thisTestID);
    if (conf.Enabled)
    {
       ... // execute test
    }
    else
    {
       // report that test was skipped due to user configuration
    }

    return c;
}

那么我错过了一些选项或者软件设计是错误的,这会阻止单元格中的测试运行而不等待其他单元格中的测试完成吗?

更新

repo是最小的控制台应用程序,用于演示此问题。

仍然有3个细胞和3个测试(任务)。在单元格1,2上我选择运行所有测试,而在单元格3上仅测试3.我期望的是在单元格3的准备任务之后,立即看到跳过的测试1,2和运行测试3.

我看到的是(# - 单元号)

#1 Preparing...
#2 Preparing...
#3 Preparing...

#1 Test1 running...
#2 Test1 running...
#3 Test1 skipped
#1 Test2 running...
#2 Test2 running...
#3 Test2 skipped
#1 Test3 running...
#2 Test3 running...
#3 Test3 running...

#2 Finalizing...
#1 Finalizing...
#3 Finalizing...
单元格3中的测试与单元格1和2中的测试同步。所有测试同时完成,而在单元格3中,单个测试应该比其他单元格更早完成。

2 个答案:

答案 0 :(得分:2)

感谢您的编辑。添加EnsureOrdered = false以阻止选项。发生的事情是你的TransfomrBlocks没有通过细胞直到他们都完成处理,所以他们可以维持你的订单。这是默认值,通常是可取的,但不是在您的情况下。

当我评论他们在当前代码中没有错时,看起来我错了。

答案 1 :(得分:0)

很难说肯定,但我可以在代码中看到两个缺点:

  1. 您没有在列表中的转换块之间传播完成
  2. 您正在使用阻止同步方法来传递消息:.Post而不是SendAsync,这显然是您在此处获取异步流所需的信息。所以最后一个必须等​​到第一个完成之前。
  3. 此外,您需要了解使用BoundedCapacity会在管道中引入限制,因此您应该检查缓冲区大小,也许很多线程只是等待队列中的某个位置变为可用。

    您可以尝试的另一件事是升级DataflowBlockOptions.MaxMessagesPerTask属性。此属性用于一个贪婪的块快速执行并处理越来越多的消息而不让其他块执行其工作的情况。在内部,每个块都有一个Task,其中正在进行处理,默认值为-1,表示无限数量的消息。通过将此值设置为某个正数,可以强制该块重新启动它的内部任务,并为其他任务提供一些空间。

    有关更多高级提示,请参阅official docs