委托是使 .NET引用中的线程更容易的一些对象。它们可用于异步调用方法。框架 4.5(或更早版本)中还有哪些其他对象可以使线程的使用更容易或更不容易出错?
还有哪些其他抽象使并发和多线程更容易?
注意:这个问题更新了这个。
我倾向于回答很多与多线程相关的问题,而且我经常看到以各种不同方式提出的相同基本问题。我将介绍多年来我看到的最常见的问题,并解释新技术如何使解决这些问题变得更容易。
关闭循环变量
这不是线程特有的问题,但使用线程肯定会放大问题。foreach
C# 5.0通过为每次迭代创建一个新变量来解决循环的这个问题。您将不再需要为 lambda 表达式闭包创建特殊变量。不幸的是,for
循环仍然需要使用特殊的捕获变量来处理。
等待异步任务完成
.NET 4.0 引入了一个CountdownEvent
类,它封装了等待许多任务完成所需的大量逻辑。大多数初级开发人员使用Thread.Join
调用或单个WaitHandle.WaitAll
调用。这两者都存在可扩展性问题。旧模式是使用一个单一的ManualResetEvent
并在计数器达到零时发出信号。使用Interlocked
该类更新了计数器。CountdownEvent
使这种模式更容易。只需记住将您的 main 也视为工作人员,以避免如果一名工作人员在所有工作人员排队之前完成,可能发生的微妙竞争条件。
.NET 4.0 还引入了Task
可以通过TaskCreationOptions.AttachedToParent
. 如果您调用Task.Wait
父母,它也会等待所有子任务完成。
生产者-消费者
.NET 4.0 引入了BlockingCollection
类似于普通队列的类,除了它可以在集合为空时阻塞。您可以通过调用对对象进行排队,Add
并通过调用将对象出列Take
。Take
阻塞,直到一个项目可用。这大大简化了生产者-消费者逻辑。过去,开发人员试图编写自己的阻塞队列类。但是,如果你不知道自己在做什么,那么你真的可以把它搞砸……糟糕。事实上,很长一段时间以来,微软在 MSDN 文档中都有一个阻塞队列示例,该示例本身就被严重破坏了。幸运的是,它已被删除。
使用工作线程进度更新 UI
BackgroundWorker
对于新手开发人员来说,从 WinForm 应用程序中分离后台任务的引入变得更加容易。主要好处是您可以ReportProgress
从DoWork
事件处理程序中调用,并且ProgressChanged
事件处理程序将自动编组到 UI 线程。当然,任何在 SO 上跟踪我的答案的人都知道我对编组操作(通过Invoke
等)作为使用简单进度信息更新 UI 的解决方案的感受。我一直在嘲笑它,因为它通常是一种糟糕的方法。BackgroundWorker
仍然迫使开发人员进入推送模型(通过后台的编组操作),但至少它在幕后完成了所有这些工作。
Invoke 的不优雅
我们都知道一个 UI 元素只能从 UI 线程访问。这通常意味着开发人员必须通过 、 或 使用编组操作ISynchronizeInvoke
,DispatcherObject
或者SynchronizationContext
将控制权转移回 UI 线程。但是让我们面对它。这些编组操作看起来很难看。Task.ContinueWith
使它更优雅一点,但真正的荣耀await
属于 C# 5 的新异步编程模型的一部分。await
可用于等待 aTask
完成,即流控制在任务运行时暂时中断,然后在正确的同步上下文中的那个位置返回。没有什么比await
用作所有这些Invoke
调用的替代品更优雅和令人满意的了。
并行编程
我经常看到一些问题,询问事情如何并行发生。旧方法是创建几个线程或使用ThreadPool
. .NET 4.0 使用了 TPL 和 PLINQ。该类Parallel
是使循环的迭代并行进行的好方法。对于普通的旧 LINQ ,PLINQAsParallel
是同一枚硬币的另一面。这些新的 TPL 特性极大地简化了这类多线程编程。
.NET 4.5 引入了 TPL 数据流库。它旨在使原本复杂的并行编程问题变得优雅。它将类抽象为块。它们可以是目标块或源块。数据可以从一个块流向另一个块。有许多不同的块,包括BufferBlock<T>
、BroadcastBlock<T>
、ActionBlock<T>
等,它们都做不同的事情。而且,当然,整个库将针对使用 newasync
和await
关键字进行优化。这是一组令人兴奋的新课程,我认为它会慢慢流行起来。
优雅终止
你如何让线程停止?我经常看到这个问题。最简单的方法是打电话Thread.Abort
,但我们都知道这样做的危险......我希望。有许多不同的方法可以安全地做到这一点。CancellationToken
.NET 4.0 引入了一个更统一的概念,称为通过和取消CancellationTokenSource
。后台任务可以轮询IsCancellationRequested
或只是ThrowIfCancellationRequested
在安全点调用以优雅地中断他们正在做的任何工作。其他线程可以调用Cancel
以请求取消。
好吧,让我们在这里看看:
ThreadPool
- 有点旧,但对于简单的生产者 - 消费者模式仍然可靠。BackgoundWorker
(.NET 2.0+) - 另一个老式结构,为在 GUI 应用程序的后台执行任务提供有用的功能。Timer
s - 用于使用后台线程以指定的时间间隔执行代码。Task
(.NET 4.0+) - 在底层线程池上运行的线程抽象,并提供许多有用的功能,如异常封送处理和调度。对于所谓的“任务并行”模式很有用。Parallel.For
, Parallel.ForEach
(.NET 4.0+) - 适合对一组数据并行执行相同的操作。对于所谓的“数据并行”模式很有用。Parallel.Invoke
(.NET 4.0+) - 对Task
s 的进一步抽象。只需并行触发几段代码(方法、lambda)。毫无疑问,掌握新的Tpl DataFlow库(包含在 .net 4.5 中)将为您在并发开发方面提供最大的推动力。
如果您认真对待高并发应用程序,请花一两天时间熟悉 DataFlow。真的很好。
Task
和Task<T>
,但它们从 .NET 4 开始就一直存在。不一定async
适用于线程,请参阅Øredev的 Jon 的视频以获得很好的解释。