【问题标题】:How can I control thread count when I use "Task.WhenAll"使用“Task.WhenAll”时如何控制线程数
【发布时间】:2015-09-10 17:37:01
【问题描述】:

我正在通过异步发出 http get 请求来验证图像 URL。下面的代码一切正常,但是当我有这么多图像时,我们的防火墙将阻止我的互联网访问,因为有太多线程同时请求。因此,我一直在寻找一种解决方案,如何限制并发运行的线程数。我最终得到了这个thread 告诉我使用SemaphoreSlim 但我不知何故无法理解这个想法以及如何实现这个?

  • 在添加任务时,SemaphoreSlim wait 或 waitAsnyc(有什么区别?)是否应该在 foreach 中?我可以像在代码中那样使用 linq 创建任务列表吗?
    • 为什么会用到task.Run?
    • 线程在执行哪一行之后开始?在 task.run 或 task.whenall 之后?

如果这不是最好的方法,请提出更好的方法。我不确定将 MaxDegreeOfParallelism 与 parallel.foreach 一起使用是否也有意义?

  Dim tasks = myImages.Select(Function(x) testUrl_async(x))
  Dim results = Await Task.WhenAll(tasks)

Async Function testUrl_async(ByVal myImage  As image) As Task(Of image)
   Dim myImageurl as string=myImage.imageurl
   myHttpResponse = Await myHttpClient.GetAsync(myImageurl)
    If myHttpResponse.IsSuccessStatusCode Then
        Return myImage
    Else
        Return Nothing
    End If
End Function

【问题讨论】:

  • 我会依次等待它们的延续或普通的旧 foreach。可以肯定的是,有一个更优雅的解决方案。
  • 为什么不用Parallel.ForEach设置MaxDegreeOfParallelism?
  • 实际上,我在下面的帖子中被建议使用更好的这种方法,我确实尝试了两种方法,但我使用此代码获得了比 parallel.foreach 更好的性能。 stackoverflow.com/questions/31007055/…
  • 我检查了那个帖子。不使用 Parallel.ForEach 很聪明。我根据您的想法发布答案。看看吧。。
  • 限制线程数是自定义 TaskScheduler 的工作,例如 Parallel Extensions Extras 中的 QueuedTaskScheduler。其他任何东西都只会浪费 ThreadPool 线程。不过,更好的选择是使用具有特定 MaxDegreeOfParallelism 的 ActionBlock。它要简单得多 并且 允许您简单地将所有 URL 发布到队列中以进行处理。

标签: vb.net multithreading async-await semaphore


【解决方案1】:

由于并发请求的线程太多,我们的防火墙将阻止我的 Internet 访问。因此,我一直在寻找一种解决方案,如何限制并发运行的线程数。

很确定防火墙正在限制您的连接数,因此您想限制您的连接数(不是线程) .

是 SemaphoreSlim wait 还是 waitAsnyc(到底有什么区别?)

Wait 是同步等待 - 它阻塞调用线程。 WaitAsync 是一个异步等待 - 它释放调用线程并在信号量可用时继续执行当前方法。

在添加任务时应该在 foreach 中吗?我可以像在代码中那样使用 linq 创建任务列表吗?

您可以采用任何一种方式:明确地建立一个列表,或者使用 LINQ。

为什么会用到task.Run?

那是那个答案的错误。 Task.Run 这里当然不需要或不需要。

线程在哪一行执行后开始?在 task.run 或 task.whenall 之后?

当您调用Task.Run 时,该委托会立即排队到线程池中。但正如我上面所说,您不想使用Task.Run(它也不应该在原始答案中使用)。


所以,这样的事情就足够了:

Private _mutex As New SemaphoreSlim(20)
Async Function testUrl_async(myImage As image) As Task(Of image)
    Await _mutex.WaitAsync()
    Try
        Dim myImageurl = myImage.imageurl
        Dim myHttpResponse = Await myHttpClient.GetAsync(myImageurl)
        Return If(myHttpResponse.IsSuccessStatusCode, myImage, Nothing)
    Finally
        _mutex.Release()
    End Try
End Function

【讨论】:

  • 把异步获取图片的逻辑和控制连接的逻辑分开不是更好吗?我认为最好让调用循环使用 Semaphore 并实现连接控制逻辑。
  • @mehrandvd:当然,你可以这样做。只需将WaitAsync/Try/Finally/Release 移动到调用代码中即可。
  • @StephenCleary 当您在另一篇文章中不使用 Task.Run 时,程序会创建任务列表列表并仅在执行 Task.WhenAll 行时执行,对吗?但是 waitAsync 在那里的作用如何,因为它已经超出了 foreach 范围?这是怎么工作的?还是这种情况下用whenAll不好?
  • @batmaci:异步任务总是“热”返回——它们已经在进行中。 awaitWhenAll 都不会启动它们。
【解决方案2】:

您可以像这样使用TaskSemaphoreSlim

var MaxAllowedConcurrentRequestsCount = 20;
var guard = new SemaphoreSlim(0, MaxAllowedConcurrentRequestsCount);
foreach(var imageUrl in imageUrls)
{
    guard.Wait()
    var image = await Task.Factory.StartNew((imageUrl) => return myHttpClient.Get(imageUrl));
    guard.Release();
    // You can do whaterver you like with image. For example add it to you concurrent list.
}

【讨论】:

  • @batmaci 您可以查看以下链接,该链接描述了为什么在某些情况下使用循环和 ContinueWith 而不是 async/await。 thinqlinq.com/Post.aspx/Title/Async-in-loops
  • @Panagiotis Kanavos 的评论发生了什么。他提到了你方法的一个缺点。我正在比较你的建议和他的建议。现在我看到他删除了他的评论。你的方法和使用 QueuedTaskScheduler 有什么大的区别吗?
  • 这也很好。我喜欢它,因为它使用了我认为很酷的 Parallel.ForEeach。我的代码写在较低级别,所以你可以看到算法。
  • @batmaci 在与斯蒂芬聊了一会儿之后,我刚刚在调用循环中使用了 await 而不是 ContinueWith。
  • 你为什么使用 task.factory 而不是 task.whenall?它在 foreach 之前或 foreach 内部有什么区别吗?
【解决方案3】:

我假设您不在 WPF 或 Windows 窗体线程中。如果您在await 之后将只有一个线程在工作。

由于我们假设您不在这些线程中,ThreadPool 用于在await 之后执行延续。您可以使用ThreadPool.SetMaxThreads 更改池使用的线程数量,但我建议您不要这样做,让.NET 来做正确的事情。这通常是最好的方法。

【讨论】:

  • 实际上我在 WPF 上在 ViewModel 中执行此操作。你是什​​么意思“如果你在等待之后只有一个线程在工作。”?你的意思是它不会是多线程?如果没有,为什么我会看到速度增加?它的运行速度明显更快
  • 如果您在 WPF 线程中,await 必须同步到 WPF Dispatcher。基本上,它确实使用Dispatcher.Invoke 在调度程序中将延续注册为操作。 msdn.microsoft.com/en-us/library/hh199416(v=vs.110).aspx
  • OP 想要同时执行多个 GET 以降低速度但不超过防火墙设置的限制。这与 WPF 或 await 之后的线程无关。
  • @batmaci 并非如此。基本上并发请求意味着同时建立到主机的多个连接。这些连接可以由同一个线程维护。因此,如果您在收到第一个请求的答案并关闭第一个连接之前打开第二个连接(并向主机请求某些内容),则会发生并发请求。
  • @batmaci 您低估了示例中连接打开的速度在您编写它的方式中,初始调用者线程将一个一个打开每个连接。一旦通信完成,ThreadPool 线程将同时处理Await myHttpClient.GetAsync(myImageurl) 之后的所有内容。但是,即使您的连接仅打开几毫秒,打开连接的第一个线程也会更快。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多