【问题标题】:How can I send HTTP GETs to several URLs using .NET async code and get the first result?如何使用 .NET 异步代码将 HTTP GET 发送到多个 URL 并获得第一个结果?
【发布时间】:2017-06-23 02:16:54
【问题描述】:

我了解异步 javascript,但 aync .NET 有不同的方法,我仍然没有正确理解它。

我有一个要检查的 URL 列表。我想异步检查它们并获取第一个返回特定状态码的。在这种情况下,我正在寻找状态代码 401(未授权),因为这表明这是一个登录挑战,这是我所期望的。所以我不能只使用Task.WaitAny,因为我需要运行一些代码来查看哪个首先与我的状态码匹配。

谁能给我一个示例,说明如何在 aync 任务上运行回调,然后在找到所需内容时停止所有其他任务?

我在这个项目中使用 .NET 4,如果可能的话,我更愿意坚持使用它。我安装了System.Net.Http.HttpClient nuget 包。

更新: 我已经整理了以下代码,我终于得到了正确的结果,除了我认为它正在等待每个任务 - 错过了异步的全部意义。不确定在内部任务中使用new Task()t.Wait(),但它似乎是捕获异常的唯一方法。 (DNS 失败和连接超时时会发生异常 - 我不知道有比捕获和忽略异常更好的方法来处理这些问题。)

关于改进此代码以使其真正异步的任何建议?

    public async Task<ActionResult> Test() {
        //var patterns = GetPatterns();
        var patterns = "http://stackoverflow.com/,https://www.google.com,http://www.beweb.co.nz,https://outlook.office365.com/Microsoft-Server-ActiveSync,http://rubishnotexist.com".Split(",").ToList();

        var httpClient = new System.Net.Http.HttpClient();
        string result = "";
        CancellationTokenSource source = new CancellationTokenSource();
        CancellationToken cancellationToken = source.Token;
        var allTasks = new List<Task>();
        foreach (var pattern in patterns) {
            var url = pattern;

            Task task = new Task(() => {
                string answer = "";
                var st = DateTime.Now;
                var t = httpClient.GetAsync(pattern, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
                t.ContinueWith(d => {
                    if (!source.IsCancellationRequested) {
                        if (t.IsFaulted) {
                            answer = "Fault - " + " " + url;
                        } else if (d.Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) {
                            // found it - so cancel all others
                            answer = "YES - " + d.Result.StatusCode + " " + url;
                            //source.Cancel();
                        } else {
                            answer = "No - " + d.Result.StatusCode + " " + url;
                        }
                    }
                    result += answer + " ("+(DateTime.Now-st).TotalMilliseconds+"ms)<br>";
                });
                try {
                    t.Wait();
                } catch (Exception) {
                    // ignore eg DNS fail and connection timeouts
                }
            });

            allTasks.Add(task);
            task.Start();
        }

        // Wait asynchronously for all of them to finish
        Task.WaitAll(allTasks.ToArray());

        return Content(result + "<br>DONE");
    }

在上面我没有取消部分工作。这是一个包含取消的版本:

    public async Task<ActionResult> Test2(string email) {
        var patterns = GetPatterns(email);
        patterns = "http://stackoverflow.com/,https://www.google.com,http://www.beweb.co.nz,https://outlook.office365.com/Microsoft-Server-ActiveSync,http://rubishnotexist.com".Split(",").ToList();
        var httpClient = new System.Net.Http.HttpClient();

        string result = "";
        CancellationTokenSource source = new CancellationTokenSource();
        CancellationToken cancellationToken = source.Token;
        var allTasks = new List<Task>();
        foreach (var pattern in patterns) {
            var url = pattern;

            Task task = new Task(() => {
                string answer = "";
                var st = DateTime.Now;
                var t = httpClient.GetAsync(pattern, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
                t.ContinueWith(d => {
                    if (!source.IsCancellationRequested) {
                        if (t.IsFaulted) {
                            answer = "Fault - " + " " + url;
                        } else if (d.Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) {
                            // found it - so cancel all others
                            answer = "YES - " + d.Result.StatusCode + " " + url;
                            result += answer + " (" + (DateTime.Now - st).TotalMilliseconds + "ms)  <-- cancelled here <br>";
                            source.Cancel();
                        } else {
                            answer = "No - " + d.Result.StatusCode + " " + url;
                        }
                    } else {
                            answer = "cancelled - " + url;
                    }
                    result += answer + " (" + (DateTime.Now - st).TotalMilliseconds + "ms)<br>";
                });
                try {
                    t.Wait();
                } catch (Exception) {
                    // ignore
                }
            });

            allTasks.Add(task);
            task.Start();
        }

        // Wait asynchronously for all of them to finish
        Task.WaitAll(allTasks.ToArray());

        return Content(result + "<br>DONE");
    }

【问题讨论】:

  • 只使用Task.WhenAll() 怎么样,然后检查任务的结果(假设您在返回值中包含 HTTP 响应状态代码)?
  • 我会尝试 Task.WhenAll 但有些请求可能需要很长时间,所以我想在找到一个请求时退出并停止其他请求。
  • 您可以创建一个CancellationToken,将其传递给所有任务,并在第一次失败时取消它。然后所有任务都会观察令牌(通过传递给支持它的异步方法,或者通过偶尔显式检查它),并在它被取消时退出。
  • 所以我会使用带有取消令牌的Task.WhenAll()
  • 不,您需要将CancellationToken 传递给所有任务。 This answer有详细解释。

标签: c# asp.net asp.net-mvc asynchronous async-await


【解决方案1】:

改用Task.WhenAll(),然后检查任务的结果。

为了防止任何人抛出异常后其他任务继续进行,您可以创建一个 CancellationToken(首先创建一个 CancellationTokenSource,然后使用其 .Token),然后将其传递给所有任务,并在失败时,您取消令牌;有关更多详细信息和示例代码,请参阅How to cancel and raise an exception on Task.WhenAll if any exception is raised?。然后所有任务都会观察令牌,并且可以选择不定期地显式检查它,如果它被取消则退出。他们还应该将它传递给那些支持它的方法,这样他们就可以在令牌被取消时快速取消。

关于例外,this answer 很好地涵盖了它们。如果您不想在调用代码中抛出异常,您应该在每个任务创建中处理异常,但是您需要相应地修改上述取消机制。相反,您可以只捕获 await Task.WhenAll() 可能抛出的单个异常,然后观察每个任务的 Task.Exception 属性中抛出的所有异常,或者如果这是所需的结果,则忽略它们。


成功后重新取消(来自 cmets)- 我想有很多方法可以做到,但一种可能是:

using (var cts = new CancellationTokenSource())
{
    var tasks = new List<Task<HttpStatusCode>>();

    foreach (var url in patterns)
    {
        tasks.Add(GetStatusCodeAsync(url, cts.Token));
    }

    while (tasks.Any() && !cts.IsCancellationRequested)
    {
        Task<HttpStatusCode> task = await Task.WhenAny(tasks);

        if (await task == HttpStatusCode.Unauthorized)
        {
            cts.Cancel();
            // Handle the "found" situation
            // ...
        }
        else
        {
            tasks.Remove(task);
        }
    }
}

然后将您的 HttpClient 代码放在单独的方法中:

private static async Task<HttpStatusCode> GetStatusCodeAsync(object url, CancellationToken token)
{
    try
    {
        // Your HttpClient code
        // ...
        await <things>;
        // (pass token on to methods that support it)
        // ...
        return httpStatusCode;
    }
    catch (Exception e)
    {
        // Don't rethrow if you handle everything here
        return HttpStatusCode.Unused; // (or whatever)
    }
}

【讨论】:

  • 感谢您的回答。一旦有人抛出异常,其他人就会停止,但我需要他们继续前进。如何创建一个包装 httpclient 任务并捕获异常的任务?我应该使用 new Task() 还是不必要地使用新线程?在创建任务时似乎没有抛出异常,但只有当我添加一个 Wait() 时,这是正确的做法吗?
  • 在真正异步的情况下,您希望完全避免 .Wait().Result 因为它们会阻塞调用。这些都由await 处理。你几乎肯定想避免new Task() 并且不应该需要它;只需调用返回TaskTask&lt;TResult&gt; 的方法即可创建、启动并最终返回任务。我建议阅读 Stephen Cleary 的 primer 和(最佳实践)[msdn.microsoft.com/en-us/magazine/jj991977.aspx);它涵盖了所有这些。
  • 如果您不希望其他调用停止,请不要使用我描述的取消机制。重新捕获HttpClient 异常,您可以在每个任务的代码中执行此操作,而不是让异常冒泡到await
  • 澄清我不希望其他调用在异常时停止。我确实希望其他呼叫在成功时停止。
  • 嗨,非常感谢。不幸的是,这在 .NET4 中不起作用,因为任务不可等待,我想这就是我陷入使用等待的原因。 Task.WhenAny 未定义。看起来我可能需要升级项目。另外,这段代码应该是什么? Your HttpClient code 以及如何将返回值 return HttpStatusCode.Unused 转换为 Task&lt;HttpStatusCode&gt;
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-10-15
  • 1970-01-01
  • 2016-09-04
  • 2018-12-27
  • 2018-12-16
  • 2023-03-06
相关资源
最近更新 更多