并发编程概述--C#并发编程经典实例

优秀软件的一个关键特征就是具备并发性。过去的几十年,咱们能够进行并发编程,可是难度很大。之前,并发性软件的编写、调试和维护都很难,这致使不少开发人员为图省事放弃了并发编程。新版.NET 中的程序库和语言特征,已经让并发编程变得简单多了。随着Visual Studio 2012 的发布,微软明显下降了并发编程的门槛。之前只有专家才能作并发编程,而今天,每个开发人员都可以(并且应该)接受并发编程。react

1.1简介

首先,我来解释几个贯穿本书始终的术语。先来介绍并发。算法

  • 并发 
    同时作多件事情

这个解释直接代表了并发的做用。终端用户程序利用并发功能,在输入数据库的同时响应用户输入。服务器应用利用并发,在处理第一个请求的同时响应第二个请求。只要你但愿程序同时作多件事情,你就须要并发。几乎每一个软件程序都会受益于并发。大多数开发人员一看到“并发”就会想到“多线程”。对这两个概念,须要作一下区分。数据库

  • 多线程 
    并发的一种形式,它采用多个线程来执行程序。

从字面上看,多线程就是使用多个线程,多线程是并发的一种形式,但不是惟一的形式。实际上,直接使用底层线程类型在现代程序中基本不起做用。比起老式的多线程机制,采用高级的抽象机制会让程序功能更增强大、效率更高所以,这里尽可能不涉及一些过期的技术。书中全部多线程的方法都采用高级类型,而不是Thread或BackgroundWorker。编程

一旦你输入new Thread(),那就糟糕了,说明项目中的代码太过期了。数组

可是,不要认为多线程已经完全被淘汰了!由于线程池要求多线程继续存在。线程池存听任务的队列,这个队列可以根据须要自行调整。相应地,线程池产生了另外一个重要的并发形式:并行处理promise

  • 并行处理 
    把正在执行的大量的任务分割成小块,分配给多个同时运行的线程。

为了让处理器的利用效率最大化,并行处理(或并行编程)采用多线程。当现代多核CPU执行大量任务时,若只用一个核执行全部任务,而其余核保持空闲,这显然是不合理的。并行处理把任务分割成小块并分配给多个线程,让它们在不一样的核上独立运行。并行处理是多线程的一种,而多线程是并发的一种。在现代程序中,还有一种很是重要但不少人还不熟悉的并发类型:异步编程缓存

  • 异步编程 
    并发的一种形式,它采用future 模式或回调(callback)机制,以免产生没必要要的线程。

一个future(或promise)类型表明一些即将完成的操做。在.NET 中,新版future 类型有Task 和Task。在老式异步编程API 中,采用回调或事件(event),而不是future。异步编程的核心理念是异步操做:启动了的操做将会在一段时间后完成。这个操做正在执行时,不会阻塞原来的线程。启动了这个操做的线程,能够继续执行其余任务。当操做完成时,会通知它的future,或者调用回调函数,以便让程序知道操做已经结束。异步编程是一种功能强大的并发形式,但直至不前,实现异步编程仍须要特别复杂的代码。VS2012 支持async 和await,这让异步编程变得几乎和同步(非并发)编程同样容易。并发编程的另外一种形式是响应式编程(reactive programming)。异步编程意味着程序启动一个操做,而该操做将会在一段时间后完成。响应式编程与异步编程很是相似,不过它是基于异步事件(asynchronous event)的,而不是异步操做(asynchronous operation)。异步事件能够没有一个实际的“开始”,能够在任什么时候间发生,而且能够发生屡次,例如用户输入。安全

  • 响应式编程 
    一种声明式的编程模式,程序在该模式中对事件作出响应。

若是把一个程序看做一个大型的状态机,则该程序的行为即可视为它对一系列事件作出响应,即每换一个事件,它就更新一次本身的状态。这听起来很抽象和空洞,但实际上并不是如此。利用现代的程序框架,响应式编程已经在实际开发中普遍使用。响应式编程不必定是并发的,但它与并发编程联系紧密,所以本书介绍了响应式编程的基础知识。一般状况下,一个并发程序要使用多种技术。大多数程序至少使用了多线程(经过线程池)和异步编程。要大胆地把各类并发编程形式进行混合和匹配,在程序的各个部分使用合适的工具。服务器

 

1.2 异步编程简介

异步编程有两大好处。第一个好处是对于面向终端用户的GUI程序:异步编程提升了响应能力。咱们都遇到过在运行时会临时锁定界面的程序,异步编程可使程序在执行任务时仍能响应用户的输入。第二个好处是对于服务器端应用:异步编程实现了可扩展性。服务器应用能够利用线程池知足其可扩展性,使用异步编程后,可扩展性一般能够提升一个数量级。markdown

现代的异步.NET程序使用两个关键字:asyncawaitasync关键字加在方法声明上,它的主要目的是使方法内的await关键字生效(为了保持向后兼容,同时引入了这两个关键字)。若是async方法有返回值,应返回Task<T>;若是没有返回值,应返回Task。这些task类型至关于future,用来在异步方法结束时通知主程序。

图像说明文字不要用void做为async方法的返回类型!async方法能够返回void,可是这仅限于编写事件处理程序。一个普通的async方法若是没有返回值,要返回Task,而不是void

有了上述背景知识,咱们来快速看一个例子:

async Task DoSomethingAsync() { int val = 13; //异步方式等待1秒 await Task.Delay(TimeSpan.FromSeconds(1)); val *= 2; //异步方式等待1秒 await Task.Delay(TimeSpan.FromSeconds(1)); Trace.WriteLine(val); }

和其余方法同样,async方法在开始时以同步方式执行。在async方法内部,await关键字对它的参数执行一个异步等待。它首先检查操做是否已经完成,若是完成了,就继续运行(同步方式)。不然,它会暂停async方法,并返回,留下一个未完成的task。一段时间后,操做完成,async方法就恢复运行。

一个async方法是由多个同步执行的程序块组成的,每一个同步程序块之间由await语句分隔。第一个同步程序块在调用这个方法的线程中运行,但其余同步程序块在哪里运行呢?状况比较复杂。

最多见的状况是,用await语句等待一个任务完成,当该方法在await处暂停时,就能够捕捉上下文(context)。若是当前SynchronizationContext不为空,这个上下文就是当前SynchronizationContext。若是当前SynchronizationContext为空,则这个上下文为当前TaskScheduler。该方法会在这个上下文中继续运行。通常来讲,运行UI线程时采用UI上下文,处理ASP.NET请求时采用ASP.NET请求上下文,其余不少状况下则采用线程池上下文。

所以,在上面的代码中,每一个同步程序块会试图在原始的上下文中恢复运行。若是在UI线程中调用DoSomethingAsync,这个方法的每一个同步程序块都将在此UI线程上运行。可是,若是在线程池线程中调用,每一个同步程序块将在线程池线程上运行。

要避免这种错误行为,能够在await中使用ConfigureAwait方法,将参数continueOnCapturedContext设为false。接下来的代码刚开始会在调用的线程里运行,在被await暂停后,则会在线程池线程里继续运行:

async Task DoSomethingAsync() { int val = 13; //异步方式等待1秒 await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); val *= 2; //异步方式等待1秒 await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); Trace.WriteLine(val.ToString()); }

图像说明文字最好的作法是,在核心库代码中一直使用ConfigureAwait。在外围的用户界面代码中,只在须要时才恢复上下文。

关键字await不只能用于任务,还能用于全部遵循特定模式的awaitable类型。例如,Windows Runtime API定义了本身专用的异步操做接口。这些接口不能转化为Task类型,但确实遵循了可等待的(awaitable)模式,所以能够直接使用await。这种awaitable类型在Windows应用商店程序中更加常见,可是在大多数状况下,await使用TaskTask<T>

有两种基本的方法能够建立Task实例。有些任务表示CPU须要实际执行的指令,建立这种计算类的任务时,使用Task.Run(如须要按照特定的计划运行,则用TaskFactory.StartNew)。其余的任务表示一个通知(notification),建立这种基于事件的任务时,使用TaskCompletionSource<T>。大部分I/O型任务采用TaskCompletionSource<T>

使用asyncawait时,天然要处理错误。在下面的代码中,PossibleExceptionAsync会抛出一个NotSupportedException异常,而TrySomethingAsync方法可很顺利地捕捉到这个异常。这个捕捉到的异常完整地保留了栈轨迹,没有人为地将它封装进TargetInvocationExceptionAggregateException类:

async Task TrySomethingAsync() { try { await PossibleExceptionAsync(); } catch(NotSupportedException ex) { LogException(ex); throw; } }

一旦异步方法抛出(或传递出)异常,该异常会放在返回的Task对象中,而且这个Task对象的状态变为“已完成”。当await调用该Task对象时,await会得到并(从新)抛出该异常,而且保留着原始的栈轨迹。所以,若是PossibleExceptionAsync是异步方法,如下代码就能正常运行:

async Task TrySomethingAsync() { //发生异常时,任务结束。不会直接抛出异常。 Task task = PossibleExceptionAsync(); try { //Task对象中的异常,会在这条await语句中引起 await task; } catch(NotSupportedException ex) { LogException(ex); throw; } }

关于异步方法,还有一条重要的准则:你一旦在代码中使用了异步,最好一直使用。调用异步方法时,应该(在调用结束时)用await等待它返回的task对象。必定要避免使用Task.WaitTask<T>.Result方法,由于它们会致使死锁。参考一下下面这个方法:

async Task WaitAsync() { //这里awati会捕获当前上下文…… await Task.Delay(TimeSpan.FromSeconds(1)); // ……这里会试图用上面捕获的上下文继续执行 } void Deadlock() { //开始延迟 Task task = WaitAsync(); //同步程序块,正在等待异步方法完成 task.Wait(); }

若是从UI或ASP.NET的上下文调用这段代码,就会发生死锁。这是由于,这两种上下文每次只能运行一个线程。Deadlock方法调用WaitAsync方法,WaitAsync方法开始调用delay语句。而后,Deadlock方法(同步)等待WaitAsync方法完成,同时阻塞了上下文线程。当delay语句结束时,await试图在已捕获的上下文中继续运行WaitAsync方法,但这个步骤没法成功,由于上下文中已经有了一个阻塞的线程,而且这种上下文只容许同时运行一个线程。这里有两个方法能够避免死锁:在WaitAsync中使用ConfigureAwait(false)(致使await忽略该方法的上下文),或者用await语句调用WaitAsync方法(让Deadlock变成一个异步方法)。

图像说明文字若是使用了async,最好就一直使用它。

若想更全面地了解关于异步编程的知识,可参阅Alex Davies(O'Reilly)编写的Async in C# 5.0,这本书很是不错。另外,微软公司有关异步编程的在线文档也很不错,建议你至少读一读“async overview”和“Task-based Asynchronous Pattern(TAP) overview”这两篇。若是要深刻了解,官方FAQ和博客上也有大量的信息。

1.3 并行编程简介

若是程序中有大量的计算任务,而且这些任务能分割成几个互相独立的任务块,那就应该使用并行编程。并行编程可临时提升CPU利用率,以提升吞吐量,若客户端系统中的CPU常常处于空闲状态,这个方法就很是有用,但一般并不适合服务器系统。大多数服务器自己具备并行处理能力,例如ASP.NET可并行地处理多个请求。某些状况下,在服务器系统中编写并行代码仍然有用(若是你知道并发用户数量会一直是少数)。但一般状况下,在服务器系统上进行并行编程,将下降自己的并行处理能力,而且不会有实际的好处。

并行的形式有两种:数据并行(data parallelism)和任务并行(task parallelim)。数据并行是指有大量的数据须要处理,而且每一块数据的处理过程基本上是彼此独立的。任务并行是指须要执行大量任务,而且每一个任务的执行过程基本上是彼此独立的。任务并行能够是动态的,若是一个任务的执行结果会产生额外的任务,这些新增的任务也能够加入任务池。

实现数据并行有几种不一样的作法。一种作法是使用Parallel.ForEach方法,它相似于foreach循环,应尽量使用这种作法。在3.1节将会详细介绍Parallel.ForEach方法。Parallel类也提供Parallel.For方法,这相似于for循环,当数据处理过程基于一个索引时,可以使用这个方法。下面是使用Parallel.ForEach的代码例子:

void RotateMatrices(IEnumerable<Matrix> matrices, float degrees) { Parallel.ForEach(matrices, matrix => matrix.Rotate(degrees)); }

另外一种作法是使用PLINQ(Parallel LINQ),它为LINQ查询提供了AsParallel扩展。跟PLINQ相比,Parallel对资源更加友好,Parallel与系统中的其余进程配合得比较好,而PLINQ会试图让全部的CPU来执行本进程。Parallel的缺点是它太明显。不少状况下,PLINQ的代码更加优美。PLINQ在3.5节有详细介绍:

IEnumerable<bool> PrimalityTest(IEnumerable<int> values) { return values.AsParallel().Select(val => IsPrime(val)); }

无论选用哪一种方法,在并行处理时有一个很是重要的准则。

图像说明文字每一个任务块要尽量的互相独立。

只要任务块是互相独立的,并行性就能作到最大化。一旦你在多个线程中共享状态,就必须以同步方式访问这些状态,那样程序的并行性就变差了。第11章将详细讲述同步。

有多种方式能够控制并行处理的输出。能够把结果存在某些并发集合,或者对结果进行聚合。聚合在并行处理中很常见,Parallel类的重载方法,也支持这种map/reduce函数。关于聚合的详细内容在3.2节。

下面讲任务并行。数据并行重点在处理数据,任务并行则关注执行任务。

Parallel类的Parallel.Invoke方法能够执行“分叉/联合”(fork/join)方式的任务并行。3.3节将详细介绍这个方法。调用该方法时,把要并行执行的委托(delegate)做为传入参数:

void ProcessArray(double[] array) { Parallel.Invoke( () => ProcessPartialArray(array, 0, array.Length / 2), () => ProcessPartialArray(array, array.Length / 2, array.Length) ); } void ProcessPartialArray(double[] array, int begin, int end) { // CPU密集型的操做…… }

如今Task这个类也被用于异步编程,但当初它是为了任务并行而引入的。任务并行中使用的一个Task实例表示一些任务。可使用Wait方法等待任务完成,还可使用ResultException属性来检查任务执行的结果。直接使用Task类型的代码比使用Parallel类要复杂,可是,若是在运行前不知道并行任务的结构,就须要使用Task类型。若是使用动态并行机制,在开始处理时,任务块的个数是不肯定的,只有继续执行后才能肯定。一般状况下,一个动态任务块要启动它所需的全部子任务,而后等待这些子任务执行完毕。为实现这个功能,可使用Task类型中的一个特殊标志:TaskCreationOptions.AttachedToParent。动态并行机制在3.4节中详述。

跟数据并行同样,任务并行也强调任务块的独立性。委托(delegate)的独立性越强,程序的执行效率就越高。在编写任务并行程序时,要格外留意下闭包(closure)捕获的变量。记住闭包捕获的是引用(不是值),所以能够在结束时以不明显地方式地分享这些变量。

对全部并行处理类型来说,错误处理的方法都差很少。因为操做是并行执行的,多个异常就会同时发生,系统会把这些异常封装在AggregateException类里,在程序中抛给代码。这一特色对全部方法都是同样的,包括Parallel.ForEachParalle.InvokeTask.Wait等。AggregateException类型有几个实用的FlattenHandle方法,用来简化错误处理的代码:

try { Parallel.Invoke(() => { throw new Exception(); }, () => { throw new Exception(); }); } catch (AggregateException ex) { ex.Handle(exception => { Trace.WriteLine(exception); return true; // "已经处理" }); }

一般状况下,不必关心线程池处理任务的具体作法。数据并行和任务并行都使用动态调整的分割器,把任务分割后分配给工做线程。线程池在须要的时候会增长线程数量。线程池线程使用工做窃取队列(work-stealing queue)。微软公司为了让每一个部分尽量高效,作了不少优化。要让程序获得最佳的性能,有不少参数能够调节。只要任务时长不是特别短,采用默认设置就会运行得很好。

图像说明文字任务不要特别短,也不要特别长。

若是任务过短,把数据分割进任务和在线程池中调度任务的开销会很大。若是任务太长,线程池就不能进行有效的动态调整以达到工做量的平衡。很难肯定“过短”和“太长”的判断标准,这取决于程序所解决问题的类型以及硬件的性能。根据一个通用的准则,只要没有致使性能问题,我会让任务尽量短(若是任务过短,程序性能会忽然下降)。更好的作法是使用Parallel类型或者PLINQ,而不是直接使用任务。这些并行处理的高级形式,自带有自动分配任务的算法(而且会在运行时自动调整)。

要更深刻的了解并行编程,这方面最好的书是Colin Campbell等人编写的Parallel Programming with Microsoft.NET(微软出版社)。

1.4 响应式编程简介

跟并发编程的其余形式相比,响应式编程的学习难度较大。若是对响应式编程不是很是熟悉,代码维护相对会更难一点。一旦你学会了,就会发现响应式编程的功能特别强大。响应式编程能够像处理数据流同样处理事件流。根据经验,若是事件中带有参数,那么最好采用响应式编程,而不是常规的事件处理程序。

响应式编程基于“可观察的流”(observable stream)这一律念。你一旦申请了可观察流,就能够收到任意数量的数据项(OnNext),而且流在结束时会发出一个错误(OnError)或一个“流结束”的通知(OnCompleted)。有些可观察流是不会结束的。实际的接口就像这样:

interface IObserver<in T> { void OnNext(T item); void OnCompleted(); void OnError(Exception error); } interface IObservable<out T> { IDisposable Subscribe(IObserver<T> observer); }

不过,开发人员不须要实现这些接口。微软的Reactive Extensions(Rx)库已经实现了全部接口。响应式编程的最终代码很是像LINQ,能够认为它就是“LINQ to events”。下面的代码中,前面是咱们不熟悉的操做符(IntervalTimestamp),最后是一个Subscribe,可是中间部分是咱们在LINQ中熟悉的操做符:WhereSelect。LINQ具备的特性,Rx也都有。Rx在此基础上增长了不少它本身的操做符,特别是与时间有关的操做符:

Observable.Interval(TimeSpan.FromSeconds(1)) .Timestamp() .Where(x => x.Value % 2 == 0) .Select(x => x.Timestamp) .Subscribe(x => Trace.WriteLine(x));

上面的代码中,首先是一个延时一段时间的计数器(Interval),随后为每一个事件加了一个时间戳(Timestamp)。接着对事件进行过滤,只包含偶数值(Where),选择了时间戳的值(Timestamp),而后当每一个时间戳值到达时,把它输入调试器(Subscribe)。若是没有理解上述新的操做符(例如Interval),没关系,咱们会在后面讲述。如今只要记住这是一个LINQ查询,与你之前见过的LINQ查询很相似。主要区别在于:LINQ to Object和LINQ to Entity使用“拉取”模式,LINQ的枚举经过查询拉出数据。而LINQ to event(Rx)使用“推送”模式,事件到达后就自行穿过查询。

可观察流的定义和其订阅是互相独立的。上面最后一个例子与下面的代码等效:

IObservable<DateTimeOffset> timestamps = Observable.Interval(TimeSpan.FromSeconds(1)) .Timestamp() .Where(x => x.Value % 2 == 0) .Select(x => x.Timestamp); timestamps.Subscribe(x => Trace.WriteLine(x));

一种常规的作法是把可观察流定义为一种类型,而后将其做为IObservable<T>资源使用。其余类型能够订阅这些流,或者把这些流与其余操做符组合,建立另外一个可观察流。

Rx的订阅也是一个资源。Subscribe操做符返回一个IDisposable,即表示订阅完成。当你响应了那个可观察流,就得处理这个订阅。

对于hot observable(热可观察流)和cold observable(冷可观察流)这两种对象,订阅的作法各有不一样。一个hot observable对象是指一直在发生的事件流,若是在事件到达时没有订阅者,事件就丢失了。例如,鼠标的移动就是一个hot observable对象。cold observable对象是始终没有输入事件(不会主动产生事件)的观察流,它只会经过启动一个事件队列来响应订阅。例如,HTTP下载是一个cold observable对象,只有在订阅后才会发出HTTP请求。

一样,全部Subscribe操做符都须要有处理错误的参数。前面的例子没有错误处理参数。下面则是一个更好的例子,在可观察流发生错误时,它能正确处理:

Observable.Interval(TimeSpan.FromSeconds(1)) .Timestamp() .Where(x => x.Value % 2 == 0) .Select(x => x.Timestamp) .Subscribe(x => Trace.WriteLine(x), ex => Trace.WriteLine(ex));

在进行Rx实验性编程时,Subject<T>这个类型颇有用。这个“subject”就像手动实现一个可观察流。能够在代码中调用OnNextOnErrorOnCompleted,这个subject会把这些调用传递给订阅者。Subject<T>用于实验时效果很是不错,但在实际产品开发时,应该使用第5章介绍的操做符。

Rx的操做符很是多,本书只介绍了一部分。想了解关于Rx的更多信息,建议阅读优秀的在线图书Introduction to Rx。

1.5 数据流简介

TPL数据流颇有意思,它把异步编程和并行编程这两种技术结合起来。若是须要对数据进行一连串的处理,TPL数据流就颇有用。例如,须要从一个URL上下载数据,接着解析数据,而后把它与其余数据一块儿作并行处理。TPL数据流一般做为一个简易的管道,数据从管道的一端进入,在管道中穿行,最后从另外一端出来。不过,TPL数据流的功能比普通管道要强大多了。对于处理各类类型的网格(mesh),在网格中定义分叉(fork)、链接(join)、循环(loop)的工做,TPL数据流都能正确地处理。固然了,大多数时候TPL数据流网格仍是被用做管道。

数据流网格的基本组成单元是数据流块(dataflow block)。数据流块能够是目标块(接收数据)或源块(生成数据),或二者皆可。源块能够链接到目标块,建立网格。链接的具体内容在4.1节介绍。数据流块是半独立的,当数据到达时,数据流块会试图对数据进行处理,而且把处理结果推送给下一个流程。使用TPL数据流的常规方法是建立全部的块,再把它们连接起来,而后开始在一端填入数据。而后,数据会自行从另外一端出来。再强调一次,数据流的功能比这要强大得多,数据穿过的同时,可能会断开链接、建立新的块并加入到网格,不过这是很是高级的使用场景。

目标块带有缓冲区,用来存放收到的数据。所以,在还来不及处理数据的时候,它仍能接收新的数据项,这就让数据能够持续地在网格上流动。在有分叉的状况下,一个源块连接了两个目标块,这种缓冲机制就会产生问题。当源块有数据须要传递下去时,它会把数据传给与它连接的块,而且一次只传一个数据。默认状况下,第一个目标块会接收数据并缓存起来,而第二个目标块就收不到任何数据。解决这个问题的方法是把目标块设置为“非贪婪”模式,以限制缓冲区的数量,这部分将在4.4节介绍。

若是某些步骤出错,例如委托在处理数据项时抛出异常,数据流块就会出错。数据流块出错后就会中止接收数据。默认状况下,一个块出错不会摧毁整个网格。这让程序有能力重建部分网格,或者对数据从新定向。然而这是一个高级用法。一般来说,你是但愿这些错误经过连接传递给目标块。数据流也提供这个选择,惟一比较难办的地方是当异常经过连接传递时,它就会被封装在AggregateException类中。所以,若是管道很长,最后异常的嵌套层次会很是多,这时就可使用AggregateException.Flatten方法:

try { var multiplyBlock = new TransformBlock<int, int>(item => { if (item == 1) throw new InvalidOperationException("Blech."); return item * 2; }); var subtractBlock = new TransformBlock<int, int>(item => item - 2); multiplyBlock.LinkTo(subtractBlock, new DataflowLinkOptions { PropagateCompletion = true }); multiplyBlock.Post(1); subtractBlock.Completion.Wait(); } catch (AggregateException exception) { AggregateException ex = exception.Flatten(); Trace.WriteLine(ex.InnerException); }

数据流错误的处理方法将在4.2节详细介绍。

数据流网格给人的第一印象是与可观察流很是相似,实际上它们确实有不少共同点。网格和流都有“数据项”这一律念,数据项从网格或流的中间穿过。还有,网格和流都有“正常完成”(表示没有更多数据须要接收时发出的通知)和“不正常完成”(在处理数据中发生错误时发出的通知)这两个概念。可是,Rx和TPL数据流的性能并不相同。若是执行须要计时的任务,最好使用Rx的observable对象,而不是数据流块。若是进行并行处理,最好使用数据流块,而不是Rx的observable对象。从概念上说,Rx更像是创建回调函数:observable对象中的每一个步骤都会直接调用下一步。相反,数据流网格中的每一块都是互相独立的。Rx和TPL数据流有各自的应用领域,也有一些交叉的领域。另外一方面,Rx和TPL数据流也很是适合同时使用。Rx和TPL数据流的互操做性将在7.7节详细介绍。

最经常使用的块类型有TransformBlock<TInput, TOutput>(与LINQ的Select相似)、TransformManyBlock<TInput, Toutput>(与LINQ的SelectMany相似)和ActionBlock<T>(为每一个数据项运行一个委托)。要了解TPL数据流的更多知识,建议阅读MSDN的文档和Guide to Implementing Custom TPL Dataflow Blocks。

1.6 多线程编程简介

线程是一个独立的运行单元,每一个进程内部有多个线程,每一个线程能够各自同时执行指令。每一个线程有本身独立的栈,可是与进程内的其余线程共享内存。对某些程序来讲,其中有一个线程是特殊的,例如用户界面程序有一个UI线程,控制台程序有一个main线程。

每一个.NET程序都有一个线程池,线程池维护着必定数量的工做线程,这些线程等待着执行分配下来的任务。线程池能够随时监测线程的数量。配置线程池的参数多达几十个,可是建议采用默认设置,线程池的默认设置是通过仔细调整的,适用于绝大多数现实中的应用场景。

应用程序几乎不须要自行建立新的线程。你若要为COM interop程序建立STA线程,就得建立线程,这是惟一须要线程的状况。

线程是低级别的抽象,线程池是稍微高级一点的抽象,当代码段遵循线程池的规则运行时,线程池就会在须要时建立线程。本书介绍的技术抽象级别更高:并行和数据流的处理队列会根据状况遵循线程池运行。抽象级别更高,正确代码的编写就更容易。

基于这个缘由,本书根本不介绍ThreadBackgroundWorker这两种类型。它们曾经很是流行,但那个时代已通过去了。

1.7 并发编程的集合

并发编程所用到的集合有两类:并发集合和不可变集合。这两种类别的集合将在第8章详细介绍。多个线程能够用安全的方式同时更新并发集合。大多数并发集合使用快照(snapshot),当一个线程在增长或删除数据时,另外一个线程也能枚举数据。比起给常规集合加锁以保护数据的方式,采用并发集合的方式要高效得多。

不可变集合则有些不一样。不可变集合其实是没法修改的。要修改一个不可变集合,须要创建一个新的集合来表明这个被修改了的集合。这看起来效率很是低,可是不可变集合的各个实例之间尽量多地共享存储区,所以实际上效率没想象得那么差。不可变集合的优势之一,就是全部的操做都是简洁的,所以特别适合在函数式代码中使用。

1.8 现代设计

大多数并发编程技术有一个相似点:它们本质上都是函数式(functional)的。这里“functional”的意思不是“实用,能完成任务”1,而是把它做为一种基于函数组合的编程模式。若是你接受函数式的编程理念,并发编程的设计就会简单得多。

1英文中“函数式”和“实用”是同一个单词functional。——译者注

函数式编程的一个原则就是简洁(换言之,就是避免反作用)。解决方案中的每个片断都用一些值做为输入,生成一些值做为输出。应该尽量避免让这些段落依赖于全局(或共享)变量,或者修改全局(或共享)数据结构。不论这个片断是异步方法、并行任务、Rx操做仍是数据流块,都应该这么作。固然了,具体作法早晚会受到计算内容的影响,但若是能用简洁的段落来处理,而后用结果来执行更新,代码就会更加清晰。

函数式编程的另外一个原则是不变性。不变性是指一段数据是不能被修改的。在并发编程中使用不可变数据的缘由之一,是程序永远不须要对不可变数据进行同步。数据不能修改,这一事实让同步变得没有必要。不可变数据也能避免反作用。在编写本书时(2014年),虽然不可变数据尚未被普遍接受,但本书中有几节会介绍不可变数据结构。

1.9 技术要点总结

在.NET刚推出时,就对异步编程提供了必定的支持。可是异步编程一直是很难的,直到2012年.NET 4.5(同时发布C# 5.0和VB 2012)引入asyncawait这两个关键字。本书中的异步编程方法,将所有采用现代的async/await。同时介绍一些方法,来实现async和老式异步编程模式的交互。要支持老式平台的话,须要下载NuGet包Microsoft.Bcl.Async

图像说明文字不要在基于.NET 4.0的ASP.NET代码中使用Microsoft.Bcl.Async进行异步编程!在.NET中,ASP.NET管道已经进行修改以支持async。对于异步ASP.NET项目,必须使用.NET 4.5或更高版本。

.NET 4.0引入了任务并行库(TPL),彻底支持数据并行和任务并行。可是一些资源较少的平台(例如手机),一般不支持TPL。TPL是.NET框架自带的。

Reactive Extensions团队已经让它尽量多地支持多种平台。Reactive Extensions和asyncawait同样,对全部类型的应用都有好处,包括客户端和服务器端应用。Rx在NuGet包Rx-Main中。

TPL数据流库只支持较新的平台,它的官方版本在NuGet包Microsoft.Tpl.Dataflow中。

并发编程的集合是.NET框架的一部分,可是不可变集合在NuGet包Microsoft.Bcl.Immutable中。表1-1列出了各主流平台对各类技术的支持状况。

表1-1:各平台对并发编程的支持

平台

async

并行编程

Rx

数据流

并发集合

不可变集合

.NET 4.5

.NET 4.0

×

×

Mono iOS/Droid

Windows Store

Windows Phone Apps 8.1

Windows Phone SL 8.0

×

×

Windows Phone SL 7.1

×

×

×

×

Silverlight 5

×

×

×

×

目录

相关文章
相关标签/搜索