我正在做的事情的概述:在一个循环中,我开始一个新的Task<string>
并将其添加到List<Task<string>>
。问题是,在返回字符串后,任务抛出System.Threading.Tasks.TaskCanceledException
,我不知道为什么。下面是我正在做的修剪版本
public async Task<string> GenerateXml(object item)
{
using (var dbContext = new DatabaseContext())
{
//...do some EF dbContext async calls here
//...generate the xml string and return it
return "my xml data";
}
}
var tasks = new List<Task<string>>();
我的循环看起来像:
foreach (var item in items)
{
tasks.Add(Task.Run(() => GenerateXml(item).ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted)));
//also tried:
tasks.Add(Task.Run(async () => await GenerateXml(item).ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted)));
//both generate the same exception
//after looking at my code, I was using the ContinueWith on the GenerateXml method call, which should still work, right?
//I moved the continue with to the `Task.Run` and still get the exception.
}
Task.WaitAll(tasks.ToArray()); //this throws the AggregateException which contains the TaskCanceledException
当我单步执行代码时,它会点击return "my xml data";
,但会抛出异常。
我想用ContinueWith
避免的是当我循环每个任务并获得结果时,它不会抛出与AggregateException
一样的WaitAll
。
这是一个工作的控制台应用程序抛出...我知道问题出在ContinueWith
,但为什么?
class Program
{
static void Main(string[] args)
{
var program = new Program();
var tasks = new List<Task<string>>();
tasks.Add(Task.Run(() => program.GenerateXml().ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted)));
Task.WaitAll(tasks.ToArray()); //this throws the AggregateException
foreach (var task in tasks)
{
Console.WriteLine(task.Result);
}
Console.WriteLine("finished");
Console.ReadKey();
}
public async Task<string> GenerateXml()
{
System.Threading.Thread.Sleep(3000);
return "my xml data";
}
}
答案 0 :(得分:3)
正如回答者Avram暗示的那样,你得到了异常,因为你的列表不包含运行GenerateXml()
方法的任务,而是那些是运行该方法的任务的延续。
由于这些任务仅在GenerateXml()
抛出异常时才会运行,如果对GenerateXml()
的任何调用成功,那么至少其中一个延续任务不会被激活跑。相反,它通过被取消(即当它的前期任务成功完成)完成,因此对WaitAll()
的调用看到取消并抛出聚合异常。
恕我直言,解决这个问题的最佳方法是坚持使用async
/ await
模式。即而不是直接使用ContinueWith()
,编写代码,使其可读和表达。在这种情况下,我会编写一个包装器async
方法来调用GenerateXml()
方法,捕获发生的任何异常,并在这种情况下返回""
值。
这是MCVE的修改版,显示了我的意思:
class Program
{
static void Main(string[] args)
{
var tasks = new List<Task<string>>();
tasks.Add(SafeGenerateXml());
Task.WaitAll(tasks.ToArray());
foreach (var task in tasks)
{
Console.WriteLine(task.Result);
}
Console.WriteLine("finished");
Console.ReadKey();
}
static async Task<string> SafeGenerateXml()
{
try
{
return await GenerateXml();
}
catch (Exception)
{
return "";
}
}
static async Task<string> GenerateXml()
{
await Task.Delay(3000);
return "my xml data";
}
}
恕我直言,这更符合C#中新的async
习语,更不容易失败,更容易理解究竟发生了什么(即完全避免ContinueWith()
,你甚至没有机会对正在等待哪些任务感到困惑,正如你在原始代码中所做的那样。)
答案 1 :(得分:2)
由于您使用.ContinueWith
作为从异常中恢复的方法,因此您只需添加try {} catch
语句即可。
var tasks = new List<Task<string>>();
foreach (var item in items)
{
var closure = item;
var task =
Task.Factory.StartNew(
async () =>
{
try
{
return await GenerateXml(closure);
}
catch (Exception exception)
{
//log
return "";
}
}).Unwrap();
tasks.Add(task);
}
Task.WaitAll(tasks.ToArray());
但如果我是你,我会将此逻辑隐藏在GenerateXml
方法中。只要您将默认值(此处为空字符串)视为有效值,就应该没问题。
var tasks = items.Select(item => Task.Run(() => GenerateXml(item))).ToList();
答案 2 :(得分:2)
您正在执行第二项任务 .ContinueWith((t)任务。
要运行正确的代码,您需要重构代码 如下所示拆分:
Task<string> t1 = Task.Run(() => program.GenerateXml());
t1.ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted);
tasks.Add(t1);
您可以重构这样的任务:(用于错误处理)
tasks.Add(program.GenerateXml().ContinueWith(t => {return t.IsFaulted? "": t.Result; }));