如何使用 cancellationTokens 释放 LongRunningTask

How to dispose a LongRunningTask using cancellationTokens

提问人:Thimo Luijsterburg 提问时间:7/27/2023 最后编辑:Thimo Luijsterburg 更新时间:7/27/2023 访问量:47

问:

与这个问题相关:我需要取消 signalR 中的长时间运行循环,我该怎么做?

我设置了一种快速的方法来更好地了解自己的情况,并找到了半个解决方案。我能够取消任务(不确定如何取消,因为 CancellationTokenSource 在 .Cancel() 但无论如何)。但似乎有一个新问题。这是我当前的测试代码:

服务器端集线器:

public class MyHub : Hub<IMyHub>
    {
        private CancellationTokenSource cancellationTokenSource;

        public void StartLongRunningTask()
        {
            cancellationTokenSource = new CancellationTokenSource();

            try
            {
                LongRunningTask(cancellationTokenSource.Token);
                // Task completed successfully
            }
            catch (TaskCanceledException)
            {
                // Task was canceled by the client
                // Handle cancellation logic here
            }
            finally
            {
                cancellationTokenSource.Dispose();
            }
        }

        public async void CancelLongRunningTask()
        {
            cancellationTokenSource?.Cancel();

            await Clients.All.SendStatus(false);
        }

        private void LongRunningTask(CancellationToken ct)
        {
            // Your long-running task implementation
            // Ensure to check the cancellation token periodically and stop processing if canceled
            while (!ct.IsCancellationRequested)
            {
                _ = Clients.Caller.SendStatus(true);

                _ = Task.Delay(1000, ct);
                // Continue with other work...
            }
        }
    }

客户端调用方:

public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;

        private readonly HubConnection _hubConnection;

        public bool Running { get; set; }
        public string Status { get; set; }

        public IndexModel(ILogger<IndexModel> logger)
        {
            _logger = logger;

            _hubConnection = new HubConnectionBuilder()
                .WithUrl("https://localhost:7127/myhub") // Replace with the actual URL of your SignalR hub
                .Build();

            _ = _hubConnection.StartAsync();

            _hubConnection.On<bool>("SendStatus", (status) =>
            {
                Running = status;
            });
        }

        public async Task OnGetAsync()
        {
            // Start the SignalR connection
        }

        public async Task<IActionResult> OnPostOpenAsync(IFormCollection data)
        {
            Status = _hubConnection.State.ToString();

            return Page();
        }

        public async Task<IActionResult> OnPostStartAsync(IFormCollection data)
        {
            _ = _hubConnection.InvokeAsync("StartLongRunningTask");

            Status = _hubConnection.State.ToString();

            return Page();
        }

        public async Task<IActionResult> OnPostCancelAsync(IFormCollection data)
        {
            await _hubConnection.InvokeAsync("CancelLongRunningTask");

            Status = _hubConnection.State.ToString();

            return Page();
        }

    }

你可以猜到打开、启动和取消功能后面有按钮。但这里发生了几件有趣的事情。

  1. 如果我等待 LongRunningTask 的调用,它会等到它完成,但永远不会等到我取消它。到目前为止,这对我来说是合乎逻辑的。

  2. 但是当我不等待 LongRunningTask(CancellationTokenSource.Token);在服务器中心。它不应该等待这个任务完成,对吧?还是我在这里遗漏了什么?

  3. 我不确定为什么,但是Clients.Caller.SendStatus(true)不会发送任何内容,直到我将其移动到服务器中心中的StartlongRunningTask()。我能想到一个原因,但我不确定,如果有人能向我解释这种行为,那就太好了。

  4. 最后但并非最不重要的一点是,我写这篇文章的核心原因。如果我在客户端等待调用,它将在我单击取消之前等待。但是当我这样做时,结果是内存和 CPU 使用率:

enter image description here

第一条蓝线是我启动长时间运行的任务的位置,第二条蓝线是我取消任务的位置。我相信不知何故,任务一直在后台运行,但一旦我单击,前端就会停止加载,并且触发了服务器集线器上的“CancelLongRunningTask”。那么这到底是怎么发生的呢?前端停止加载,但在后台保持循环?如果有人能向我解释这种行为,那就太好了!

编辑

在等待了 Canton7 建议的一切之后,还有一个问题需要回答,因为 CancelLongRunningTask() 函数在空令牌上调用 Cancel?除此之外,.SendStatus(true) 从不发送?enter image description here

C# 循环 SignalR 取消 CancellationTokenSource

评论

0赞 canton7 7/27/2023
请注意,由于您选中了 ,因此您可能会(也可能不会,具体取决于此测试还是第一个发现取消已发生的测试)通过返回而不是抛出异常来响应取消。在这种情况下,您的不会运行。如果您使用异常来处理取消,最好避免检查和使用!ct.IsCancellationRequestedTask.Delay// Handle cancellation logic herect.IsCancellationRequestedct.ThrowIfCancellationRequested()
2赞 canton7 7/27/2023
另一个问题是你没有 ,所以你会尽可能快地绕着这个循环旋转,连续调用。您无需等待对任何一方的调用,因此您甚至不必等待上一次发送完成再发送另一次发送awaitTask.DelaywhileClients.Caller.SendStatus(true)SendStatus
1赞 canton7 7/27/2023
前端可能停止加载,因为它被连续的状态流发送了垃圾邮件!
2赞 canton7 7/27/2023
你的客户端代码中也有很多。警告说“你应该等待这项任务”的存在是有原因的!你通常不应该像这样压制它——你给自己带来了其他问题_ =
1赞 canton7 7/27/2023
所以,仔细想想。如果它是暂时性的,那么每次请求实例时,你都会得到一个新实例。因此,您调用的实例不会与您调用的实例相同。字段特定于实例。如果您调用了一个您没有调用的实例,那么当然将是空的......StartLongRunningTaskCancelLongRunningTaskCancelLongRunningTaskStartLongRunningTaskcancellationTokenSource

答:

0赞 canton7 7/27/2023 #1

请注意,由于您选中了 ,因此您可能会(也可能不会,具体取决于此测试还是第一个发现取消已发生的测试)通过返回而不是抛出异常来响应取消。在这种情况下,您的不会运行。如果您使用异常来处理取消,最好避免检查和使用 。!ct.IsCancellationRequestedTask.Delay// Handle cancellation logic herect.IsCancellationRequestedct.ThrowIfCancellationRequested()

另一个问题是你没有 ,所以你会尽可能快地绕着这个循环旋转,连续调用。您无需等待对任何一方的调用,因此您甚至不需要等待上一次发送完成再发送另一次发送。awaitTask.DelaywhileClients.Caller.SendStatus(true)SendStatus

前端可能停止加载,因为它被连续的状态流发送垃圾邮件。

让我们把事情做好,并放入适当的.这将阻止您的循环旋转,并意味着正确等待完成,而不会阻塞线程。asyncawaitswhileStartLongRunningTaskLongRunningTask

我之所以这样做,是因为被调用者可能不想等到完成。我们捕获所有异常(使用 ),因此该方法不会使我们的应用程序崩溃。StartLongRunningTaskasync voidLongRunningTaskcatch (Exception ex)async void

我还抓住了哪个是基类并涵盖更多案例。OperationCanceledExceptionTaskCanceledException

public class MyHub : Hub<IMyHub>
{
    private CancellationTokenSource cancellationTokenSource;

    public async void StartLongRunningTask()
    {
        cancellationTokenSource = new CancellationTokenSource();

        try
        {
            await LongRunningTask(cancellationTokenSource.Token);
            // Task completed successfully
        }
        catch (OperationCanceledException)
        {
            // Task was canceled by the client
            // Handle cancellation logic here
        }
        catch (Exception ex)
        {
            // Handle failure here
        }
        finally
        {
            cancellationTokenSource.Dispose();
        }
    }

    public async void CancelLongRunningTask()
    {
        cancellationTokenSource?.Cancel();

        await Clients.All.SendStatus(false);
    }

    private async Task LongRunningTask(CancellationToken ct)
    {
        // Your long-running task implementation
        // Ensure to check the cancellation token periodically and stop processing if canceled
        while (true)
        {
            ct.ThrowIfCancellationRequested();
            await Clients.Caller.SendStatus(true);

            await Task.Delay(1000, ct);
            // Continue with other work...
        }
    }
}

评论

0赞 Thimo Luijsterburg 7/27/2023
编辑了这个问题,你能为我澄清这些问题吗?