【问题标题】:How to force an ActionBlock to complete fast如何强制 ActionBlock 快速完成
【发布时间】:2020-02-08 08:09:06
【问题描述】:

根据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 毫秒后的取消。

【问题讨论】:

  • 您要求Thread.Abort 行为,而不是取消。 TPL 与合作取消一起工作。这是避免泄漏和同步问题所必需的。如果您希望延迟取消,请将 CancellationToken 传递给它。如果您希望管道中的所有块突然取消,请将 CancellationToken 传递给所有块。
  • 我没有投反对票,但您正在尝试以不应该工作的方式使用该库。您不需要这样做 - Dataflow 来自 CCR,这是一个 Robotics 平台 - 一个具有相当严格的实时要求的库。在这些情况下,中止是一种诅咒——它们使飞机坠毁,机械臂与人类相撞。与合作取消相比,Thread.Abort 的方式抛出异常非常慢。 应用程序代码必须符合域,而不是相反。
  • 这意味着 action 必须注意在需要取消时快速退出,而不是阻止。无论如何可以做什么?扔掉并可能半途而废?还是返回并执行在后台运行的任务?如果动作需要提前退出,则必须正确编写并检查取消令牌本身
  • @PanagiotisKanavos await Task.Delay(1000) 只是一个例子。我的实际工作量是一个不可取消的 API。它不接受CancelationToken 参数,我无法在它启动后停止或中止它。在取消请求的情况下我想要发生的是忽略当前正在运行的工作项。我已经不在乎了,为什么还要等呢?不用担心,没有飞机会坠毁,也不会有人受到失灵机械臂的伤害。我的应用程序只是使用慢速打印机打印一些发票。 :-)。
  • 我已经发布了一个建议,如果我没有误解这个问题,那就是。正如你已经知道的那样,我还不是很精通使用 async/await,但我觉得这可以使用 async/await 进一步简化 ;-)。如果您可以编辑我的帖子以正确使用 async/await 以达到相同的效果,请随时改进!

标签: c# cancellation tpl-dataflow


【解决方案1】:

根据您的评论摘录...:

如果出现取消请求,我希望忽略当前正在运行的工作项。我已经不在乎了,为什么还要等呢?

...假设您真的可以让任务运行,您可以简单地将您希望调用的作业 包装在另一个 Task 中,这将 不断轮询取消或完成,然后取消 那个 Task。看一下下面的“概念验证”代码,它将“长时间运行”的任务包装在另一个“任务”任务中,不断轮询包装的任务以完成,并使用 CancellationToken 取消(完全“刺激” the-moment" 状态,你当然会想重新调整一下):

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

被包装的任务显然是在它自己的好时机完成的。如果您对此没有问题(通常情况下不是),希望这能满足您的需求。

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

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 Dataflow 库,所以这当然只是一种解决方法。此外,如果您只有一个同步方法调用,而您根本没有任何影响,那么您显然需要 两个 任务。一个包装器任务用于包装同步调用,另一个用于包装包装器任务以包括连续状态轮询和取消检查。

【讨论】:

  • 是的,这是个好主意。将我的不可取消 API 方法的可取消包装器传递给我的问题可能是一个很好的解决方案!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-17
相关资源
最近更新 更多