如何清理 C# 任务 API 上的挂起任务

本文关键字:任务 挂起 API 何清理 | 更新日期: 2023-09-27 18:36:24

我有一个简单的函数如下:

    static Task<A> Peirce<A, B>(Func<Func<A, Task<B>>, Task<A>> a)
    {
        var aa = new TaskCompletionSource<A>();
        var tt = new Task<A>(() => 
            a(b =>
            {
                aa.SetResult(b);
                return new TaskCompletionSource<B>().Task;
            }).Result
        );
        tt.Start();
        return Task.WhenAny(aa.Task, tt).Result;
    }

这个想法很简单:对于a的任何实现,它必须向我返回一个Task<A>。为此,它可能会也可能不使用参数(类型 Func<A, Task<B> )。如果是这样,我们的回调将被调用并设置aa的结果,然后aa.Task将完成。否则,a的结果将不依赖于其参数,因此我们只需返回其值。在任何情况下,aa.Taska的结果都将完成,因此除非 do 不使用其参数和块,或者由 a 块返回的任务,否则它不应阻塞。

上面的代码有效,例如

    static void Main(string[] args)
    {
        Func<Func<int, Task<int>>, Task<int>> t = a =>
        {
            return Task.FromResult(a(20).Result + 10);
        };
        Console.WriteLine(Peirce(t).Result); // output 20
        t = a => Task.FromResult(10);
        Console.WriteLine(Peirce(t).Result); // output 10
    }

这里的问题是,两个任务aa.Tasktt一旦确定了WhenAny的结果就必须清理干净,否则恐怕会有挂任务的漏水。我不知道该怎么做,任何人都可以提出一些建议吗?或者这实际上不是问题,C# 会为我做吗?

附言这个名字Peirce来自命题逻辑中著名的"皮尔斯定律"(((A->B)->A)->A)。

更新:问题的重点不是"处置"任务,而是阻止它们运行。我已经测试过,当我将"主"逻辑放在 1000 个循环中时,它运行缓慢(大约 1 个循环/秒),并创建大量线程,因此这是一个需要解决的问题。

如何清理 C# 任务 API 上的挂起任务

Task是托管对象。 除非引入非托管资源,否则不必担心Task泄漏资源。 让 GC 清理它,让终结器处理WaitHandle

编辑:

如果要取消任务,请考虑以CancellationTokenSource的形式使用合作取消。 您可以通过重载将此令牌传递给任何任务,在每个任务中,您可能有一些代码,如下所示:

while (someCondition)
{
    if (cancelToken.IsCancellationRequested)
        break;
}

这样,您的任务就可以正常清理而不会引发异常。 但是,如果您调用cancelToken.ThrowIfCancellationRequested(),则可以传播OperationCancelledException。 因此,在您的情况下,您的想法是,首先完成的任何内容都可以向其他任务发出取消,这样它们就不会挂断工作。

感谢@Bryan Crosby的回答,我现在可以实现如下函数:

    private class CanceledTaskCache<A>
    {
        public static Task<A> Instance;
    }
    private static Task<A> GetCanceledTask<A>()
    {
        if (CanceledTaskCache<A>.Instance == null)
        {
            var aa = new TaskCompletionSource<A>();
            aa.SetCanceled();
            CanceledTaskCache<A>.Instance = aa.Task;
        }
        return CanceledTaskCache<A>.Instance;
    }
    static Task<A> Peirce<A, B>(Func<Func<A, Task<B>>, Task<A>> a)
    {
        var aa = new TaskCompletionSource<A>();
        Func<A, Task<B>> cb = b =>
        {
            aa.SetResult(b);
            return GetCanceledTask<B>();
        };
        return Task.WhenAny(aa.Task, a(cb)).Unwrap();
    }

而且效果很好:

    static void Main(string[] args)
    {
        for (int i = 0; i < 1000; ++i)
        {
            Func<Func<int, Task<String>>, Task<int>> t = 
                async a => (await a(20)).Length + 10;
            Console.WriteLine(Peirce(t).Result); // output 20
            t = async a => 10;
            Console.WriteLine(Peirce(t).Result); // output 10
        }
    }

现在它速度很快,不会消耗太多资源。 如果您不使用 async/await 关键字,它可以更快(在我的机器中大约 70 次):

    static void Main(string[] args)
    {
        for (int i = 0; i < 10000; ++i)
        {
            Func<Func<int, Task<String>>, Task<int>> t =
                a => a(20).ContinueWith(ta => 
                    ta.IsCanceled ? GetCanceledTask<int>() : 
                    Task.FromResult(ta.Result.Length + 10)).Unwrap();
            Console.WriteLine(Peirce(t).Result); // output 20
            t = a => Task.FromResult(10);
            Console.WriteLine(Peirce(t).Result); // output 10
        }
    }

问题是,即使你可以检测到a(20)的返回值,也没有办法取消async块而不是抛出OperationCanceledException,它会阻止WhenAny被优化。

更新:优化的代码,并比较了异步/等待和本机任务API。

更新:如果我能编写以下代码,那将是理想的:

static Task<A> Peirce<A, B>(Func<Func<A, Task<B>>, Task<A>> a)
{
    var aa = new TaskCompletionSource<A>();
    return await? a(async b => {
        aa.SetResult(b);
        await break;
    }) : await aa.Task;
}

在这里,如果 a 成功,await? a : b 具有值a的结果,如果 a 被取消,则具有值 b(如 a ? b : ca 的结果的值应具有相同类型的 b)。 await break将取消当前的异步块。

正如MS并行编程团队的Stephen Toub所说:"不。不要费心处理你的任务。

tldr:在大多数情况下,释放任务不会执行任何操作,当任务实际分配了非托管资源时,其终结器将在收集任务对象时释放这些资源。