从不同的请求中取消和创建任务

Cancelling and creating tasks from different requests

在一个 ASP.net 核心应用程序中,我在一个请求中生成后台任务,这些任务可以自行完成,也可以被第二个请求取消。我有以下实现,但我觉得应该有更好的方法来实现我想要的?有人对这个问题有经验吗?

public sealed class SomeService
{
    private record BackgroundTask(Task Task,
        CancellationTokenSource CancellationTokenSource);

    private readonly ConcurrentDictionary<Guid, BackgroundTask> _backgroundTasks
        = new();

    public void Start(Guid id)
    {
        var cancellationTokenSource = new CancellationTokenSource();

        var cancellationToken = cancellationTokenSource.Token;

        var task = Task.Run(async () =>
        {
            try
            {
                await Task.Delay(1000, cancellationToken);
            }
            finally
            {
                _backgroundTasks.TryRemove(id, out _);
            }
        }, cancellationToken);

        _backgroundTasks.TryAdd(id, new BackgroundTask(task, cancellationTokenSource));
    }

    public void Cancel(Guid id)
    {
        _backgroundTasks.TryGetValue(id, out var backgroundTask);

        if (backgroundTask == null)
        {
            return;
        }
        
        backgroundTask.CancellationTokenSource.Cancel();

        try
        {
            backgroundTask.Task.GetAwaiter().GetResult();
        }
        catch (OperationCanceledException e)
        {
            // todo: cancellation successful...
        }
    }
}

下面的代码中存在竞争条件:

var task = Task.Run(async () =>
{
    try
    {
        await Task.Delay(1000, cancellationToken);
    }
    finally
    {
        _backgroundTasks.TryRemove(id, out _);
    }
}, cancellationToken);

_backgroundTasks.TryAdd(id, new BackgroundTask(task, cancellationTokenSource));

理论上可能 task 完成后会被添加到 _backgroundTasks 中,因此它永远不会从字典中删除。

这个问题的一个解决方案是创建一个冷 Task,并且 Start 只有在它被添加到字典中之后。您可以查看此技术的示例 here。使用冷任务是一项低级且棘手的技术,所以要小心!

另一种解决方案可能是根本不使用字典,而是通过引用而不是 id 来识别 BackgroundTask。您不需要公开内部 BackgroundTask 类型。您可以 return 对其进行 object 引用,例如 所示。但我猜你有某种理由将 BackgroundTask 存储在一个集合中,以便你可以跟踪它们。