使用TPL批量/去并行化单独的调用

时间:2015-11-11 23:36:36

标签: c# .net async-await task-parallel-library reentrancy

也许TPL不是正确的工具,但至少从一个不熟悉它的人看来,它似乎应该有我想要的东西。我愿意接受那些不使用它的答案。

给出这样的方法:

public Task Submit(IEnumerable<WorkItem> work)

这可以对项目集合执行昂贵的异步操作。通常,调用者会批量处理这些项目并尽可能多地提交,并且这些批次之间存在相当长的延迟,因此它可以相当有效地执行。

然而,在某些情况下,不会发生外部批处理,Submit会快速连续多次调用少量项目(通常只有一项),甚至可能同时来自不同的线程。

我想做的是推迟处理(在累积参数的同时),直到有一定的时间没有调用,然后执行整批操作,按原定的顺序。

或者换句话说,每次调用该方法时,它都应该将其参数添加到待处理项列表中,然后从零重新启动延迟,这样在处理任何内容之前都需要一定的空闲时间。

我不希望批量限制(所以我不认为BatchBlock是正确答案),我只想要延迟/超时。我确定调用模式是这样的,在某个时刻成为空闲时段。

我不确定是否最好推迟第一次通话,或者是否应该立即开始操作,如果操作仍在进行中,则只推迟后续通话。

如果它使问题更容易,我可以使Submit返回void而不是Task(即无法在完成时观察)。

我确信我可以混淆一些像这样工作的东西,但它似乎应该已经存在于某个地方。谁能指出我正确的方向? (但我不想使用非核心库。)

1 个答案:

答案 0 :(得分:0)

好的,所以由于缺乏找到合适的东西,我最终自己实现了一些东西。似乎可以做到这一点。 (我实际上比实际代码中显示的更加一般,所以我可以更容易地重复使用它,但这说明了这个概念。)

private readonly ConcurrentQueue<WorkItem> _Items
    = new ConcurrentQueue<WorkItem>();
private CancellationTokenSource _CancelSource;

public async Task Submit(IEnumerable<WorkItem> items)
{
    var cancel = ReplacePreviousTasks();

    foreach (var item in items)
    {
        _Items.Enqueue(item);
    }

    await Task.Delay(TimeSpan.FromMilliseconds(250), cancel.Token);
    if (!cancel.IsCancellationRequested)
    {
        await RunOperation();
    }
}

private CancellationTokenSource ReplacePreviousTasks()
{
    var cancel = new CancellationTokenSource();
    var old = Interlocked.Exchange(ref _CancelSource, cancel);
    if (old != null)
    {
        old.Cancel();
    }
    return cancel;
}

private async Task RunOperation()
{
    var items = new List<WorkItem>();
    WorkItem item;
    while (_Items.TryDequeue(out item))
    {
        items.Add(item);
    }

    // do the operation on items
}

如果在250ms内发生多次提交,则先前的提交将被取消,并且在250ms启动后对所有项目执行一次操作(从最新提交开始计算)。

如果在操作运行时发生了另一次提交,它将继续运行而不取消(它很可能会从后面的调用中窃取一些项目,但没关系)。

(技术上检查cancel.IsCancellationRequested并不是必需的,因为上面的await会在延迟期间被取消时抛出异常。但它没有伤害,并且有一个小窗口它可能会抓住。)