【问题标题】:Task.Factory.ContinueWhenAny continue when any task finish without exceptionTask.Factory.ContinueWhenAny 在任何任务无异常完成时继续
【发布时间】:2015-09-11 22:12:09
【问题描述】:

我的应用程序中有 3 个任务负责从数据库中获取数据。
到现在我已经一个接一个地执行了所有的任务。如果第一次完成并有结果,那么这是我的数据,如果现在我开始第二个任务并再次检查。

最近我发现我可以使用Task.Factory.ContinueWhenAny 启动多个任务并在其中一个任务完成时继续。如果我的所有任务都没有抛出任何异常,这可以正常工作,但如果任何任务失败,我将无法获得我想要的结果。

例如:

var t1 = Task.Factory.StartNew(() =>
{
    Thread.Sleep(5000);
    return 1;
});

var t2 = Task.Factory.StartNew(() =>
{
    Thread.Sleep(2000);
    throw new Exception("My error");
    return 2;
});

var t3 = Task.Factory.StartNew(() =>
{
    Thread.Sleep(4000);
    return 3;
});

Task.Factory.ContinueWhenAny(new[] {t1, t2,t3}, (t) =>
{
    Console.WriteLine(t.Result);
});

此代码启动 3 个任务并等待其中一个任务完成。 因为t2 在 2 秒后抛出异常,所以它是 ContinueWhenAny 中可用的异常。

从上面的代码中,我想在t.Result 中获得 3。
是否只有在任务成功完成后才能继续?类似Task.Factory.ContinueWhenAnyButSkipFailedTasks

编辑 1 这是我现在基于@Nitram 回答的解决方案:

var t1 = Task.Factory.StartNew(() =>
{
    var rnd = new Random();
    Thread.Sleep(rnd.Next(5,15)*1000);
    throw new Exception("My error");
    return 1;
});

var t2 = Task.Factory.StartNew(() =>
{
    Thread.Sleep(2000);
    throw new Exception("My error");
    return 2;
});

var t3 = Task.Factory.StartNew(() =>
{
    throw new Exception("My error");
    return 3;
});

var tasks = new List<Task<int>> { t1, t2, t3 };

Action<Task<int>> handler = null;

handler = t =>
{
    if (t.IsFaulted)
    {
        tasks.Remove(t);
        if (tasks.Count == 0)
        {
            throw new Exception("No data at all!");
        }
        Task.Factory.ContinueWhenAny(tasks.ToArray(), handler);
    }
    else
    {
        Console.WriteLine(t.Result);
    }
};

Task.Factory.ContinueWhenAny(tasks.ToArray(), handler);

我现在需要的是当所有任务都抛出异常时如何抛出异常?
也许这可以更改为返回任务的单一方法 - 类似于子任务?

【问题讨论】:

  • 我认为您需要编写自己的 Task.WhenAny 版本,该版本采用 Func 指定此特定任务完成是否应该完成WhenAny。

标签: c# async-await task-parallel-library .net-4.5


【解决方案1】:

ContinueWhenAny 函数有一个重载,可以执行您想要的操作。

只需将TaskContinuationOptions 设置为OnlyOnRanToCompletion,失败的任务将被忽略。

Task.Factory.ContinueWhenAny(new[] {t1, t2,t3}, (t) =>
{
    Console.WriteLine(t.Result);
}, TaskContinuationOptions.OnlyOnRanToCompletion);

所以我们得出结论,这个答案实际上是错误的。

从列表中删除任务似乎是我能想到的唯一方法。 我试图把它放到一些代码行中。给你:

var tasks = new List<Task> {t1, t2, t3};

Action<Task> handler = null;
handler = (Task t) =>
{
    if (t.IsFauled) {
        tasks.Remove(t);
        Task.Factory.ContinueWhenAny(tasks.ToArray, handler);
    } else {
        Console.WriteLine(t.Result);
    }
};
Task.Factory.ContinueWhenAny(tasks.ToArray, handler);

我对C#不是很坚定,但我希望它能给你一个想法。基本上发生的情况是,每次处理有故障的任务时,都会从已知任务列表中删除该任务,并且该函数等待下一个。

好的,现在是 .NET 4.5 和 async-await 模式的全部内容。 await 基本上使您能够将等待之后写入的内容注册为延续。

所以这与async-await 的模式几乎相同。

var tasks = new List<Task> {t1, t2, t3};
while (tasks.Any()) 
{
    var finishedTask = await Task.WhenAny(tasks);
    if (finishedTask.IsFaulted)
    {
        tasks.Remove(finishedTask);
    }
    else
    {
        var result = await finishedTask;
        Console.WriteLine(result);
        return;
    }
}

唯一的区别是外部函数需要是一个async 函数。这意味着在遇到第一个await 时,外部函数将返回包含延续的Task。

您可以添加一个环绕函数,在此函数完成之前一直阻塞。 async-await 模式使您能够编写“看起来”像简单的同步代码的异步非阻塞代码。

另外我建议你使用Task.Run 函数来生成你的任务,而不是TaskFactory。以后会解决一些问题。 ;-)

【讨论】:

  • 感谢您这么快的回答,但不幸的是我收到错误It is invalid to exclude specific continuation kinds for continuations off of multiple tasks.
  • 啊...如果您没有正确阅读文档,就会发生这种情况。对不起,这个答案是错误的。因为 NotOn* 和 OnlyOn* 选项对于这个函数是非法的。因为我唯一的想法是维护您等待的任务列表,如果该功能触发失败的任务,您可以删除这个并等待剩余的。
  • 你能展示一些示例代码吗?这会很有帮助。
  • 您是否有任何明确的理由要使用此架构?还是您考虑使用 async-await 模式?
  • @Misiu 我在答案中添加了一些代码,可能会让您知道如何解决它。我在 2 分钟内写了这个,我对 C# 不是很好。所以请在那里执行任何错误。我更像是一个 VB 人。
【解决方案2】:

如果您使用的是 .NET 4.5,则可以使用 Task.WhenAny 轻松实现您想要的:

public async Task<int> GetFirstCompletedTaskAsync()
{
    var tasks = new List<Task> 
    {
        Task.Run(() =>
        {
            Thread.Sleep(5000);
            return 1;
        }),
        Task.Run(() =>
        {
            Thread.Sleep(2000);
            throw new Exception("My error");
        }),
        Task.Run(() =>
        {
            Thread.Sleep(4000);
            return 3;
        }),
    };

    while (tasks.Count > 0)
    {
        var finishedTask = await Task.WhenAny(tasks);
        if (finishedTask.Status == TaskStatus.RanToCompletion)
        {
            return finishedTask
        }

        tasks.Remove(finishedTask);
    }
    throw new WhateverException("No completed tasks");
}

【讨论】:

  • 我需要稍微不同的版本 - 我需要从所有任务中获得第一个结果,但只能从完成的任务中获得。如果它们都出现故障,我想抛出异常。
  • 这个任务的数量也有二次运行时间。延续的二次内存消耗。
  • @usr 请解释一下为什么这有二次运行时和内存消耗?
  • @YuvalItzchakov 他也试图将其归咎于我的回答。 ;-)
  • WhenAny 连接所有 N 个任务的延续,循环运行 N 次。在另一个答案中,有 N 个 Remove 操作,每个操作需要 N/2 个操作。
【解决方案3】:

如果只是这样做会怎么样(至少它对我有用):

        bool taskFinishedFlag = false;

        Task t1 = Task.Factory.StartNew(() => { Thread.Sleep(4000); return 1; });

        Task t2 = Task.Factory.StartNew(() => { Thread.Sleep(2000); 
                                                throw new Exception("");return 2; });

        Task t3 = Task.Factory.StartNew(() => { Thread.Sleep(4000); return 3; });

        Task<int>[] Tasks = new[] { t1, t2, t3 };

        for (int i = 0; i < Tasks.Length; i++)
        {
            Tasks[i].ContinueWith((t) =>
                {
                    if (taskFinishedFlag) return;
                    taskFinishedFlag = true;
                    Console.WriteLine(t.Result);
                }, TaskContinuationOptions.NotOnFaulted);
        }      

【讨论】:

  • 这是最好的方法。不用写入控制台,而是使用 TaskCompletionSource 来生成一个可以等待的任务。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-15
  • 2014-09-21
  • 1970-01-01
相关资源
最近更新 更多