2014-12-03 78 views
0

对于ContinueWith的工作方式我很困惑,它似乎阻止了ThreadPool并按顺序运行任务。以下面我写的代码示例:为什么ContinueWith显示为顺序阻塞和/或正在运行任务

  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不会调用数据库。

+1

对'ContinueWith'的调用几乎肯定不会被阻止。实际操作可能会顺序运行;如果他们是,这是由于他们正在做的事情的性质,而不是'ContinueWith'是如何实现的。这就是为什么你看到“睡眠”呼叫被并行化的原因。 – Servy 2014-12-03 18:44:37

+0

@Servy啊,我明白你的意思了。你如何解释这样一个事实,即任务> 50在其他任务完成之前不会运行,因为它们应该在不同的线程上运行。 (我不知道你是否有机会看到我的更新) – 2014-12-03 18:55:12

+0

这只是说明一些任务似乎在等待其他任务。你不能有无限的平行度。我并不感到惊讶,因为你不能有超过50个并行的数据库调用,我也不希望有更多的并行运行带来性能上的好处。 – Servy 2014-12-03 19:00:37

回答

0

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

因此,使一个Parallel.ForEach()可能最终导致您的应用程序停止运行,因为数据库连接相对昂贵,而且实际上打开一堆数据库连接是不可行的。使用不同的模式可能会更好,例如将item对象放入ConcurrentQueueConcurrentStack中,并针对此集合抛出3-5个线程(或更多 - 您将不得不在这里尝试)并让它们运行,直到堆栈或队列被耗尽。通过这种方式,您可以将实例化数据库连接的数量保持为安全数量,并且仍然可以避免这种令人讨厌的阻塞。

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

编辑:你不必为我的建议使用旧的语法。一堆任务可以做得很好。