如何在Task.WaitAll中处理被取消的任务和任务异常?

3

我正在使用TPL来爬取一组URL并进行一些处理。

for (int i = 0; i < list.Count; i++)
{
    var tuple = list[i];
    string url = tuple.Item2;

    tasks[i] = httpClient.GetStringAsync(url).
        ContinueWith(task => {
        {
            ......

        });
}
Task.WaitAll(tasks);

问题是,在Task.WaitAll语句处,通常会因为任务被取消而抛出异常。我理解httpClient.GetStringAsync并不总是保证成功,所以我想在异常发生时在httpClient.GetStringAsync中添加重试逻辑。如何正确地处理这个问题呢?


如果重试失败,你是否仍希望出现异常? - i3arnon
@I3arnon:我知道。但是这样会将异常作为AggregateException传递给Task.WaitAll吗?没错吧? - derekhh
是的,因为一个任务可能包含多个异常。如果你使用为async-await构建的await Task.WhenAll,那么你将会得到其中的实际异常(第一个异常)。 - i3arnon
问题在于我正在Main()函数中使用此函数,因此无法使用await Task.WhenAll。 - derekhh
如果这是至关重要的话,您可以使用Task.WhenAll (...).GetAwaiter().GetResult() - i3arnon
我理解httpClient.GetStringAsync可能并不总是保证成功,这是什么意思?你遇到了什么异常? - Yuval Itzchakov
2个回答

2

您可以使用for循环轻松地将重试包装在GetStringAsync周围,直到没有异常或达到重试限制为止。我使用await存储任务并从中提取结果,因此如果达到了重试限制但未成功,则会重新抛出异常:

async Task<string> GetStringAsync(HttpClient client,string url, int retries)
{
    Task<string> task = null;
    for (int i = 0; i < retries; i++)
    {
        try
        {
            task = client.GetStringAsync(url);
            await task;
            break;
        }
        catch
        {
            // log
        }
    }

    return await task;
}

你甚至可以将其作为HttpClient的扩展方法:

你可以将这个方法作为HttpClient的扩展方法:

static async Task<string> GetStringAsync(this HttpClient client, string url, int retries);

1
如果您不想使用 async/await,您可以使用以下扩展方法作为起点。
static class HttpClientExtentions
{
    public static Task<string> GetStringWithRetryAsync(this HttpClient client, string url, int retries)
    {
        var completion = new TaskCompletionSource<string>();
        var ex = new List<Exception>();

        Task<string> getString = client.GetStringAsync(url);
        Action<Task<string>> continueAction = null;
        continueAction = (t) =>
        {
            if (t.IsFaulted)
            {
                ex.Add(t.Exception);

                if (retries-- > 0)
                {
                    getString = client.GetStringAsync(url);
                    getString.ContinueWith(continueAction);
                }
                else
                {
                    completion.SetException(new AggregateException(ex));
                }
            }
            else // assume success, you could also handle cancellation
            {
                completion.SetResult(t.Result);
            }

        };

        getString.ContinueWith(continueAction);

        return completion.Task;
    }
}

Use it this way:

for (int i = 0; i < list.Count; i++)
{
    var tuple = list[i];
    string url = tuple.Item2;

    int retryCount = 3;
    var httpClient = new HttpClient(); // should create new object for each req
    tasks[i] = httpClient.GetStringWithRetryAsync(url, retryCount).
        ContinueWith(task => {
        {
            //......

        });
}
Task.WaitAll(tasks);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接