使用异步调用时异常处理的良好模式 [英] Good pattern for exception handling when using async calls
问题描述
我想使用 Web API,我看到很多人推荐 System.Net.Http.HttpClient
.
I want to consume an Web API and I see many people recommending System.Net.Http.HttpClient
.
那很好...但我只有 VS-2010,所以我现在还不能使用 async/await
.相反,我想我可以将 Task
与 ContinueWith
结合使用.所以我尝试了这段代码:
That's fine... but I have only VS-2010, so I cannot use async/await
just yet. Instead, I guess I could use Task<TResult>
in combination to ContinueWith
. So I tried this piece of code:
var client = new HttpClient();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
client.GetStringAsync(STR_URL_SERVER_API_USERS).ContinueWith(task =>
{
var usersResultString = task.Result;
lbUsers.DataSource = JsonConvert.DeserializeObject<List<string>>(usersResultString);
});
我的第一个观察是意识到如果 URL 不可用它不会产生任何错误,但也许会有更多这样的错误......
My first observation was to realize that it doesn't generate any error if URL is not available, but maybe there will be more errors like this...
所以我试图找到一种方法来处理此类异步调用的异常(特别是对于 HttpClient).我注意到任务"具有 IsFaulted
属性和一个 AggregateException
可能可以使用,但我还不确定如何使用.
So I am trying to find a way to handle exceptions for such async calls (particularly for HttpClient). I noticed that "Task" has IsFaulted
property and an AggregateException
which maybe could be used, but I am not sure yet how.
另一个观察结果是 GetStringAsync
返回 Task
,但 GetAsync
返回 Task
.后者可能更有用,因为它提供了一个 StatusCode
.
Another observation was that GetStringAsync
returns Task<string>
, but GetAsync
returns Task<HttpResponseMessage>
. The latter could be maybe more useful, since it presents a StatusCode
.
你能分享一个关于如何正确使用异步调用并以好的方式处理异常的模式吗?基本解释也将不胜感激.
Could you share a pattern on how to use the async calls correctly and handle exceptions in a good way? Basic explanation would be appreciated as well.
推荐答案
对于成功和失败的场景,我不会使用单独的 ContinueWith
延续.我宁愿在一个地方处理这两种情况,使用 try/catch
:
I would not use a separate ContinueWith
continuation for successful and faulted scenarios. I'd rather handle both cases in a single place, using try/catch
:
task.ContinueWith(t =>
{
try
{
// this would re-throw an exception from task, if any
var result = t.Result;
// process result
lbUsers.DataSource = JsonConvert.DeserializeObject<List<string>>(result);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
lbUsers.Clear();
lbUsers.Items.Add("Error loading users!");
}
},
CancellationToken.None,
TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext()
);
如果 t
是一个非通用的 Task
(而不是一个 Task
),你可以做 t.GetAwaiter().GetResult()
在 ContinueWith
lambda 中重新抛出原始异常;t.Wait()
也可以.准备好处理AggregatedException
,你可以用这样的东西进入内部异常:
If t
is a non-generic Task
(rather than a Task<TResult>
), you can do t.GetAwaiter().GetResult()
to re-throw the original exception inside the ContinueWith
lambda; t.Wait()
would work too. Be prepared to handle AggregatedException
, you can get to the inner exception with something like this:
catch (Exception ex)
{
while (ex is AggregatedException && ex.InnerException != null)
ex = ex.InnerException;
MessageBox.Show(ex.Message);
}
如果您正在处理一系列ContinueWith
,通常您不必处理每个 ContinueWith
内部的异常.对最外层的结果任务执行一次,例如:
If you're dealing with a series of ContinueWith
, usually you don't have to handle exceptions inside each ContinueWith
. Do it once for the outermost resulting task, e.g.:
void GetThreePagesV1()
{
var httpClient = new HttpClient();
var finalTask = httpClient.GetStringAsync("http://example.com")
.ContinueWith((task1) =>
{
var page1 = task1.Result;
return httpClient.GetStringAsync("http://example.net")
.ContinueWith((task2) =>
{
var page2 = task2.Result;
return httpClient.GetStringAsync("http://example.org")
.ContinueWith((task3) =>
{
var page3 = task3.Result;
return page1 + page2 + page3;
}, TaskContinuationOptions.ExecuteSynchronously);
}, TaskContinuationOptions.ExecuteSynchronously).Unwrap();
}, TaskContinuationOptions.ExecuteSynchronously).Unwrap()
.ContinueWith((resultTask) =>
{
httpClient.Dispose();
string result = resultTask.Result;
try
{
MessageBox.Show(result);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
},
CancellationToken.None,
TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
}
在您访问内部任务的结果 (taskN.Result
) 时,内部任务中抛出的任何异常都将传播到最外层的 ContinueWith
lambda.
Any exceptions thrown inside inner tasks will propagate to the outermost ContinueWith
lambda as you're accessing the results of the inner tasks (taskN.Result
).
这段代码很实用,但也很丑陋且不可读.JavaScript 开发人员称之为末日回调金字塔.他们有承诺来处理它.C# 开发人员有 async/await
,很遗憾,由于 VS2010 限制,您无法使用.
This code is functional, but it's also ugly and non-readable. JavaScript developers call it The Callback Pyramid of Doom. They have Promises to deal with it. C# developers have async/await
, which you're unfortunately not able to use because of the VS2010 restrain.
IMO,与 TPL 中的 JavaScript Promises 最接近的是 Stephen Toub 的 Then
模式.与 C# 4.0 中的 async/await
最接近的是他在同一博客文章中的 Iterate
模式,该模式使用 C# yield
功能.
IMO, the closest thing to the JavaScript Promises in TPL is Stephen Toub's Then
pattern. And the closest thing to async/await
in C# 4.0 is his Iterate
pattern from the same blog post, which uses the C# yield
feature.
使用 Iterate
模式,可以以更易读的方式重写上述代码.请注意,在 GetThreePagesHelper
中,您可以使用所有熟悉的同步代码语句,例如 using
、for
、while
、try/catch
等等.但是理解这种模式的异步代码流很重要:
Using the Iterate
pattern, the above code could be rewritten in a more readable way. Note that inside GetThreePagesHelper
you can use all the familiar synchronous code statements like using
, for
, while
, try/catch
etc. It is however important to understand the asynchronous code flow of this pattern:
void GetThreePagesV2()
{
Iterate(GetThreePagesHelper()).ContinueWith((iteratorTask) =>
{
try
{
var lastTask = (Task<string>)iteratorTask.Result;
var result = lastTask.Result;
MessageBox.Show(result);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
throw;
}
},
CancellationToken.None,
TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
}
IEnumerable<Task> GetThreePagesHelper()
{
// now you can use "foreach", "using" etc
using (var httpClient = new HttpClient())
{
var task1 = httpClient.GetStringAsync("http://example.com");
yield return task1;
var page1 = task1.Result;
var task2 = httpClient.GetStringAsync("http://example.net");
yield return task2;
var page2 = task2.Result;
var task3 = httpClient.GetStringAsync("http://example.org");
yield return task3;
var page3 = task3.Result;
yield return Task.Delay(1000);
var resultTcs = new TaskCompletionSource<string>();
resultTcs.SetResult(page1 + page1 + page3);
yield return resultTcs.Task;
}
}
/// <summary>
/// A slightly modified version of Iterate from
/// http://blogs.msdn.com/b/pfxteam/archive/2010/11/21/10094564.aspx
/// </summary>
public static Task<Task> Iterate(IEnumerable<Task> asyncIterator)
{
if (asyncIterator == null)
throw new ArgumentNullException("asyncIterator");
var enumerator = asyncIterator.GetEnumerator();
if (enumerator == null)
throw new InvalidOperationException("asyncIterator.GetEnumerator");
var tcs = new TaskCompletionSource<Task>();
Action<Task> nextStep = null;
nextStep = (previousTask) =>
{
if (previousTask != null && previousTask.Exception != null)
tcs.SetException(previousTask.Exception);
if (enumerator.MoveNext())
{
enumerator.Current.ContinueWith(nextStep,
TaskContinuationOptions.ExecuteSynchronously);
}
else
{
tcs.SetResult(previousTask);
}
};
nextStep(null);
return tcs.Task;
}
这篇关于使用异步调用时异常处理的良好模式的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!