我一直在研究处理多线程的.NET应用程序的不同方法,但变得有些混乱了。
新线程 -> 当仅需要一个附加线程时?
线程池 -> 当需要多个线程时。使用现有线程并将优化(基于所涉及工作的数量)留给框架更加节省成本。
任务 -> 当您需要一个额外的线程,并且恰好在使用.NET 4.0或更高版本时。这是新线程的API吗?
Parallel.for -> 当您有多个任务并希望框架根据CPU核心数为不同任务分配工作的优化时。
在MSDN上,并未说明 new Thread 方法已过时?
我一直在研究处理多线程的.NET应用程序的不同方法,但变得有些混乱了。
新线程 -> 当仅需要一个附加线程时?
线程池 -> 当需要多个线程时。使用现有线程并将优化(基于所涉及工作的数量)留给框架更加节省成本。
任务 -> 当您需要一个额外的线程,并且恰好在使用.NET 4.0或更高版本时。这是新线程的API吗?
Parallel.for -> 当您有多个任务并希望框架根据CPU核心数为不同任务分配工作的优化时。
在MSDN上,并未说明 new Thread 方法已过时?
Thread
和ThreadPool
的基本理解是基本正确的。但是,Task
则更加复杂。Task
仅表示异步操作。它可以在ThreadPool
线程上执行(如果您是调度任务的人,则通常会使用TaskScheduler.Default
),也可以在单独的非线程池线程上执行(当您指定TaskCreationOptions.LongRunning
时)。它甚至可以在没有线程的情况下执行(考虑异步IO操作:http://blog.stephencleary.com/2013/11/there-is-no-thread.html)。最后,当一个Task
封装多个异步操作时,它可以是以上任何一种的组合。Task
在Thread
和ThreadPool
之上添加了一层抽象(在适当的情况下内部使用它们,而在调度任务时,您对此有一定程度的控制)。在三种线程方法中,Task
应该是您的首选,因为它周围构建了语言支持。使用Task
组合异步操作序列与使用低级别线程工具相比轻而易举。Thread
和ThreadPool
是真正过时的-仅仅是它们不应该是您新开发的首选。抛开它们在使用Task
时仍然由框架在内部使用的事实,想想有多少完全功能的生产代码使用了Thread
或ThreadPool
,以及如果将其中任何一种类型标记为ObsoleteAttribute
会发生什么。这将是一团糟。Task
调度工作时,您考虑何时使用ThreadPool
或新的Thread
仍然适用。这里的指南很简单:
TaskCreationOptions.LongRunning
标志。这类似于启动新线程。Task.Run
将其推送到ThreadPool
中(使用默认任务调度程序,即当前实现中的ThreadPoolTaskScheduler
)。这类似于使用ThreadPool.QueueUserWorkItem(WaitCallback)
。Parallel
现在让我们谈谈Parallel
类及其成员。首先,您有 Parallel.Invoke
,它大致相当于并行启动一堆任务,然后阻塞等待它们完成。只有在处理多个阻塞操作需要并行运行时才会非常有用。如果是这种情况,并且您不想处理多个Task
实例,请使用它。
Parallel.For
和Parallel.ForEach
有些不同,主要用于对集合元素进行并行化的CPU绑定工作(由于内置的“负载均衡”方式,对于IO工作来说使用它们不是一个好主意,最终会产生太多线程,除非您限制并行度)。这些方法在概念上与Thread
、ThreadPool
和Task
有所不同,并且更接近于PLINQ。在将集合元素处理并行化导致可衡量的性能提升的情况下,使用它们通常可以代替for
或foreach
循环。
Parallel.ForEach
的委托体内阻塞了异步(IO)操作,导致负载均衡器失控 - 这正是我在最后一段谈到的确切问题(您也碰巧使用了我提到的解决方法)。这在Stephen Toub的《并行编程模式》第33页中有所讨论 - 以及一个适当的修复方法,即在涉及IO绑定工作时避免使用Parallel.ForEach
,而是使用批处理的Task
,最终实现更有效的资源使用和更好的可伸缩性。 - Kirill Shlenskiy