1

我有一个简单的功能如下:

    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.Task或结果a将完成,因此它永远不应该阻塞,除非 a 不使用它的参数和块,或者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# 会为我做这件事?

PS 这个名字Peirce来源于((A->B)->A)->A命题逻辑中著名的“皮尔士定律”( )。

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

4

3 回答 3

3

ATask是一个托管对象。除非您引入非托管资源,否则您不应该担心Task资源泄漏。让 GC 清理它并让终结器处理WaitHandle.

编辑:

如果要取消任务,请考虑以CancellationTokenSource. 您可以通过重载将此令牌传递给任何任务,并且在每个任务内部,您可能有一些代码如下:

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

这样,您的任务就可以优雅地清理而不会引发异常。但是,OperationCancelledException如果您调用cancelToken.ThrowIfCancellationRequested(). 因此,您的想法是,无论先完成什么,都可以取消其他任务,这样他们就不会挂断工作。

于 2012-06-07T04:55:44.253 回答
1

感谢@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优化。

更新:优化代码并比较 async/await 和本机 Task 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 : bvaluea的结果,如果 a 被取消,则有 value b(如a ? b : ca结果的值应该与 b 的类型相同)。 await break将取消当前的异步块。

于 2012-06-08T03:33:13.930 回答
0

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

tldr:在大多数情况下,释放任务什么都不做,当任务实际分配了非托管资源时,它的终结器将在收集任务对象时释放它们。

于 2012-06-07T04:50:06.557 回答