【问题标题】:Why does ContinueWith appear to be blocking and / or running tasks sequentially为什么 ContinueWith 似乎是按顺序阻塞和/或运行任务
【发布时间】:2014-12-03 18:12:18
【问题描述】:

我对 ContinueWith 的工作方式感到困惑,它似乎阻止了 ThreadPool 并按顺序运行任务。以我编写的以下代码示例为例:

         var items = new List<int>();

         for (int i = 0; i < 100; i++)
         {
             items.Add(i);
         }

         Parallel.ForEach(items, item =>
             {
               Task.Factory.StartNew(() =>
                 {

                 }).ContinueWith(t =>
                 {
                     if (item < 50)
                     {
                         System.Console.WriteLine("Blocking in {0}", item.ToString());
                         var x = SomeLongRunningDatabaseCall(10001);
                     }
                     System.Console.WriteLine("Item {0}", item);
                 });
             });

代码的目的是反映我在生产应用程序中遇到的可疑线程阻塞问题。令我惊讶的是,我发现问题似乎与我对 ContinueWith 的使用有关。有趣的是,如果我在 ContinueWith 上设置选项 TaskContinuationOptions.LongRunning 它会异步运行任务而不会阻塞,我也在我的生产应用程序中这样做了,这已经解决了这个问题。

但是,我真的很困惑,想更好地理解为什么没有选项 TaskContinuationOptions.LongRunning 的 ContinueWith 语句会导致任务阻塞或看起来按顺序运行,即使我在每次迭代项目时都调用一个新线程。我唯一能想到的是 ContinueWith 不是在执行前面任务的线程中运行,而是在可能导致阻塞的主线程上运行。

任何帮助或建议将不胜感激。

更新

另外有趣的是,如果我替换

    SomeLongRunningDatabaseCall(10001) 

类似

    Thread.Sleep(600000) 

它不会因为调用数据库而阻塞,但由于它在自己的线程中运行,因此至少不应该对其他任务造成任何阻塞,因为大于 50 的任务不会调用数据库。

【问题讨论】:

  • ContinueWith 的调用几乎可以肯定没有阻塞。实际操作很可能是按顺序运行的;如果是这样,那是由于他们正在做的事情的性质,而不是ContinueWith 的实施方式。这就是您看到 Sleep 调用被并行化的原因。
  • @Servy 啊,我明白你的意思了。考虑到它们应该在单独的线程上运行,您如何解释 > 50 的任务在其他任务完成之前不会运行这一事实。 (不知道你有没有机会看到我的更新)
  • 这只是说明某些任务似乎正在等待其他任务。您不能拥有无限程度的并行性。对于不能超过 50 个并行数据库调用,我当然并不感到惊讶,我也不希望更多的并行运行会带来性能优势。
  • @Servy 我现在看到了,谢谢。因此,通过在 ContinueWith 上设置 TaskContinuationOptions.LongRunning,您是否会说每个线程都在单独的线程池中运行,因此看起来好像没有阻塞。
  • 它会导致创建新线程,而不是使用线程池。如果线程池阻碍了你,那么这会影响它。如果阻塞来自其他地方,那么它不会。

标签: c# multithreading task-parallel-library


【解决方案1】:

每个线程都应该使用它自己的数据库连接对象。我知道的大多数数据库连接不支持非锁定多线程模式。在这些数据库连接中,线程被阻塞并不罕见,因为单个连接将按顺序而不是并行执行调用。

在这种情况下,创建 Parallel.ForEach() 可能最终只会让您的应用程序崩溃,因为数据库连接(相对)昂贵,而且实际上打开一大堆数据库连接是不可行的。使用不同的模式可能会更好,例如将 item 对象放入 ConcurrentQueueConcurrentStack 并针对此集合抛出 3-5 个线程(或更多 - 你必须在这里进行试验)和让这些运行直到堆栈或队列耗尽。这样,您可以将实例化数据库连接的数量保持在安全数量,同时仍然避免这种讨厌的阻塞。

所有这些让我认为使用Parallel.ForEach 进行实际的数据库执行(这里不是讨论LINQ-to-SQL 或EF,而是实际的数据库连接调用,如SqlCommand.Execute)可能不是最佳的。

编辑:您不必为我的建议使用旧语法。一组任务也可以做到这一点。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-30
    • 2013-10-18
    • 1970-01-01
    • 1970-01-01
    • 2020-12-21
    • 2022-07-12
    相关资源
    最近更新 更多