如何强制ActionBlock快速完成

时间:2019-10-10 20:06:25

标签: c# cancellation tpl-dataflow

根据documentation

  

当数据流块当前不处理消息并且已保证不再处理任何消息时,则视为已完成。

在我的情况下,这种行为并不理想。我希望能够随时取消作业,但是每个动作的处理都需要很长时间。因此,当我取消令牌时,效果不是立即的。我必须等待当前处理的项目完成。我无法直接取消操作,因为我使用的API不可取消。我可以做些什么使该块忽略当前正在运行的动作并立即完成吗?

这是一个演示我的问题的示例。令牌在500毫秒后被取消,并且每个操作的持续时间为1000毫秒:

static async Task Main()
{
    var cts = new CancellationTokenSource(500);
    var block = new ActionBlock<int>(async x =>
    {
        await Task.Delay(1000);
    }, new ExecutionDataflowBlockOptions() { CancellationToken = cts.Token });
    block.Post(1); // I must wait for this one to complete
    block.Post(2); // This one is ignored
    block.Complete();
    var stopwatch = Stopwatch.StartNew();
    try
    {
        await block.Completion;
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine($"Canceled after {stopwatch.ElapsedMilliseconds} msec");
    }
}

输出:

  

在1035毫秒后取消

所需的输出将是在约500毫秒后取消。

1 个答案:

答案 0 :(得分:1)

根据您的评论摘录...

  

在取消请求的情况下,我想忽略当前正在运行的工作项。我不再关心它了,那为什么我要等待它?

...并假设您确实可以继续运行任务,则可以将要调用的任务包裹在另一个Task 中将恒定轮询取消或完成,并取消那个 Task。看看下面的“概念验证”代码,该代码将“长期运行”任务包装在另一个“已任务”任务中,并不断轮询已包装的任务以完成任务,并使用CancellationToken进行取消(完全“刺激”任务)。时刻”状态,您当然需要对其进行重新调整):

public class LongRunningTaskSource
{
    public Task LongRunning(int milliseconds)
    {
        return Task.Run(() =>
        {
            Console.WriteLine("Starting long running task");
            Thread.Sleep(3000);
            Console.WriteLine("Finished long running task");
        });
    }

    public Task LongRunningTaskWrapper(int milliseconds, CancellationToken token)
    {
        Task task = LongRunning(milliseconds);

        Task wrapperTask = Task.Run(() =>
        {
            while (true)
            {
                //Check for completion (you could, of course, do different things
                //depending on whether it is faulted or completed).
                if (!(task.Status == TaskStatus.Running))
                    break;

                //Check for cancellation.
                if (token.IsCancellationRequested)
                {
                    Console.WriteLine("Aborting Task.");

                    token.ThrowIfCancellationRequested();
                }
            }

        }, token);

        return wrapperTask;
    }
}

使用以下代码:

static void Main()
{
    LongRunningTaskSource longRunning = new LongRunningTaskSource();

    CancellationTokenSource cts = new CancellationTokenSource(1500);

    Task task = longRunning.LongRunningTaskWrapper(3000, cts.Token);

    //Sleep long enough to let things roll on their own.
    Thread.Sleep(5000);

    Console.WriteLine("Ended Main");
}

...产生以下输出:

Starting long running task
Aborting Task.
Exception thrown: 'System.OperationCanceledException' in mscorlib.dll
Finished long running task
Ended Main

包装好的任务显然在自己的好时机中完成了。如果您对此没有问题(通常不是情况),希望这符合您的需求。

作为补充示例,运行以下代码(让包装的Task在超时之前完成):

static void Main()
{
    LongRunningTaskSource longRunning = new LongRunningTaskSource();

    CancellationTokenSource cts = new CancellationTokenSource(3000);

    Task task = longRunning.LongRunningTaskWrapper(1500, cts.Token);

    //Sleep long enough to let things roll on their own.
    Thread.Sleep(5000);

    Console.WriteLine("Ended Main");
}

...产生以下输出:

Starting long running task
Finished long running task
Ended Main

因此,任务在超时之前开始和结束,并且无需取消任何操作。当然,等待期间没有任何障碍物。当然,您可能已经知道,如果您了解长期运行的代码在幕后使用了什么,则有必要进行清理。

希望您可以修改此示例,以将类似的内容传递给ActionBlock。

免责声明和注释

我对TPL数据流库不熟悉,因此,这当然只是一种解决方法。另外,例如,如果您拥有的只是对您完全没有任何影响的同步方法调用,那么您显然将需要两个任务。一个包装器任务包装了该同步调用,另一个包装器任务包装了该包装器任务以包括连续状态轮询和取消检查。