.NET4并行(多核)编程系列
.NET 4 并行(多核)编程系列之一入门介绍 本系列文章将会对.NET 4 中的并行编程技术(也称之为多核编程技术)以及应用作全面的介绍。本篇文章的议题如下:1.并行编程和多线程编程的区别。2.并行编程技术的利弊 3.何时采用并行编程 1.并行编程和多线程编程的区别。1.1 并行编程。现在随着多核计算机的普及,并行编程技术,也就是多核编程技术也逐渐称为开发的主流。为此,在.NET 4 中就引入了“并行编程”。在.NET 4 中一些列的 Library 和类为并行编程提供了支持,如:Task Parallel Library,Parallel LINQ 等。其实在.NET 1.0 中就有了并行编程技术的一些实现-多线线程技术。多线程最大的问题就是难于使用和管理。在使用多线程的使用,因为它的复杂性,往往使用我们把注意力分散了多线程上。而致使我们的最初目的被掩盖了。1.2 比较区别 在.NET 4 中提出的并处编程的底层机制其实还是基于多线程的。但是他们之前最大的区别就是.NET 4 中的并行编程更加方便。在传统的编程模型中,程序员负责创建线程,为线程分配任务,管理线程。一个形象的比喻:你拥有一批士兵,然后你对他们下达命令,之后,你就必须时刻监视你的那些士兵,确保他们按照你的意图办事。(很累吧!)在.NET 4 中的并行编程是依赖 Task Parallel Library(后面简称为 TPL)实现的。在 TPL中,最基本的执行单元是 task(中文可以理解为任务),一个 task 就代表了你要执行的一个操作。你可以为你所要执行的每一个操作定义一个 task,TPL 就负责创建线程来执行你所定义的task,并且管理线程。TPL 是面向 task 的,自动的;而传统的多线程是以人工为导向的。Task 机制使得我们把注意力关注在我们要解决的问题上面。如果之前的多线程技术使得我们放弃了一些并行编程的使用,那么.NET 4 中的新的并行编程技术可以让我们重新建立信心。虽然有了新的并行技术,但是传统的多线程的技术还是很有用的。当我们使用 TPL 中的并行技术的时候来执行多个 task 的时候,我们不用在关心底层创建线程,管理线程等。2.并行编程技术的利弊 使用并行技术最大的好处就是提高了系统的性能。并行处理过程一般是这样的:一个要执行的任务被拆分为很多很小的部分,然后这些很小的部分就分别在不同处理器(可以是多核的一 .台电脑,也可以使很多的电脑)上执行。因为这些很多很小的部分同时在执行,所以称之为并行。使用并处编程的时候需要考虑下面的问题:1.开销问题。并行执行不是免费的,也是要开销的。在并行运行开始和管理都是需要开销的,就类比在线程的创建和管理一样。在程序中,你要执行的任务越多,那么使用并行的效果就越好。2.数据的协调 如果在并行执行的那些小部分需要共享公共的数据,那么我们就要协调。一般来说,需要协调的数据越多,并行执行的性能损耗就越大。如果执行各个小部分之间都是独立的,那么我们就不用协调了。但是很多的时候,我们都是需要协调的。而且协调的技术也不是很难,在之后的文章中会一一讲述。3.性能提高多少 增加一台计算机的 CPU 可能会提高程序的运行速度,但是不是绝对的。我们知道,一个应用程序在单核的计算机上运行的时间不一定(往往也不是)双核计算机的 1/2.所以,采用并行编程不一定就一定会成倍的提高程序的性能。因为性能与很多的因数有关的,硬件就是很大的因数。3.何时采用并行编程 建议:如果一个问题能够用并行编程解决,那么就用,否则就不用。听起来好像是废话,但是确实是一个很不错的建议。因为并行编程也不是万能的,也只能 解决一类的问题,所以在用之前要分析问题了。如果一个问题确实能够用并行的方案来解决,但是有很多的因数影响,如之前我们提到的一些问题。权衡使用之后的开销和好处在决定是否使用。后续文章会给出很多的例子。.NET 4 并行(多核)编程系列之二 从 Task 开始 前言:我们一步步的从简单的开始讲述,还是沿用我一直的方式:慢慢演化,步步为营。本篇文章的议题如下:1.Task 基础介绍 2.Task 的创建 3.获取 Task 的执行结果 4.补充细节 1.Task 基础介绍 .首先我们还是来看看一段简单的代码:这里展示的只是一段简单的代码,不能显示出并行编程的特点。但是我们还是从最基本的开始看,慢慢进入深一点的话题。如果你曾经用过.NET 中的多线程编程,比较一下,就会发现:这段代码虽然在底层还是使用了多线程,但是写法上却简化了很多,一行代码就实现了一个并行编程。下面我们就从 Task 类开始谈。Task 类是 Task Programming Library(TPL)中最核心的一个类,下面我将会像大家展示如何使用一些方法来创建不同类型的 Task,取消 Task,等待 Task 执行完成,获取 Task 执行后的结果和对异常进行处理。在开始讨论之前,我们首先快速的看看之前的代码:这个命名空间将会是我们之后在讲述并行编程经常使用的一个。这个空间包含了很多与并行编程有关的类。还有一个要你使用的命名空间是:System.Threading,大家对这个应该比较熟悉了,之前的多线程编程常常使用到,这个空间下包含了一些在并行编程中用来协调数据的一些类。上面代码中,最主要的代码如下:Task.Factory.StartNew()=Console.WriteLine(Hello World););我们用静态方法:Task.Factory.StartNew()来创建了一个最简单的 Task-在屏幕上打印一句话。这段代码确实简单,而且都没有任何输入和需要返回的结果。下面我们就正式进入议题:2.Task 的创建 如果只是创建一个简单的 Task,我们只要为该 Task 提供一个执行体就行了,执行体可以是一个委托 delegate 或者 action。我们之前展示的那段代码就是采用了 lambda 表达式来作为Task 的执行体。.2.1 创建一个简单的 Task 为了执行一个简单的 Task,一般进行以下步骤:首先,要创建一个 Task 类的实例,然后,传入一个 System.Action 委托,这个委托中的方法就是这个 Task 运行时你要执行的方法,而且这个委托必须作为 Task 构造函数的一个参数传入。我们在传入委托作为参数的时候有多种方式:传入匿名委托,Lambda 表达式或者一个显示什么方法的委托。最后,调用 Task 实例的 Start()方法来运行。当这个 Task 实例开始运行的时候,它就被传给了内部的一个 task scheduler,这个scheduler 负责把我们创建的 task 交给底下的线程去执行。下面就看看代码:代码 using System;using System.Threading.Tasks;namespace Listing_02 class Listing_02 static void Main(string args)/use an Action delegate and a named method Task task1=new Task(new Action(printMessage);/use a anonymous delegate Task task2=new Task(delegate printMessage(););/use a lambda expression and a named method Task task3=new Task()=printMessage();/use a lambda expression and an anonymous method Task task4=new Task()=printMessage(););.task1.Start();task2.Start();task3.Start();task4.Start();/wait for input before exiting Console.WriteLine(Main method complete.Press enter to finish.);Console.ReadLine();static void printMessage()Console.WriteLine(Hello World);不知道大家注意到了没有,上面代码创建 Task 的方法和我们之前的第一段代码的创建 Task的方法不同。在之前我们采用的是 Task.Factory.StartNew()方法来创建的,这个方法创建Task 并且开始运行 Task,其实两端代码的结果是一样的,这里给出一点建议:如果这是想简单的创建一个 Task,那么使用 Factory.NewStart()来创建,很简便,如果像对所创建的 Task附加更多的定制和设置特定的属性,那么还是得一步一步的按照我们说的那些步骤来。(详细的我们后续会介绍的)2.1 为创建的 Task 传入参数 我们之前提过,在创建 Task 的时候,我们在构造函数中传入了一个 System.Action 的委托,如果我们想要把一些参数传入到 Task 中,那么我 们可以传入 System.Action的委托,其中的那个 object 就是我们传入的参数。还是给大家举个例子:代码 using System;using System.Threading.Tasks;namespace Listing_04 class Listing_04 static void Main(string args)string messages=First task,Second task,Third task,Fourth task;.foreach(string msg in messages)Task myTask=new Task(obj=printMessage(string)obj),msg);myTask.Start();/wait for input before exiting Console.WriteLine(Main method complete.Press enter to finish.);Console.ReadLine();static void printMessage(string message)Console.WriteLine(Message:0,message);注意:我们在传入参数后,必须把参数转换为它们原来的类型,然后再去调用相应的方法。例子中,因为System.Action 对应的方法是 printMessage()方法,而这个方法的要求的参数类型是 string,所以要转换为 string。想向 Task 传入参素,只能用 System.Action 3.获取 Task 的执行结果 如果要获取 Task 的结果,那么在创建 Task 的时候,就要采用 Task来实例化一个Task,其中的那个 T 就是 task 执行完成之后返回结果的类型。之后采用 Task 实例的 Result属性就可以获取结果。代码显示如下:代码 static void Main(string args)/create the task Task task1=new Task()=int sum=0;for(int i=0;i 100;i+).sum+=i;return sum;);task1.Start();/write out the result Console.WriteLine(Result 1:0,task1.Result);Console.ReadLine();只有在 task 执行完成之后,才能获取到 Result 的值。下面的代码展示了如何通过 Task.Factory.StartNew()创建一个 Task,并且获取结果:代码 static void Main(string args)/create the task Task task1=Task.Factory.StartNew()=int sum=0;for(int i=0;i 100;i+)sum+=i;return sum;);/write out the result Console.WriteLine(Result 1:0,task1.Result);Console.ReadLine();4.补充细节 在创建 Task 的时候,Task 有很多的构造函数的重载,一个主要的重载就是传入TaskCreateOptions 的枚举:TaskCreateOptions.None:用默认的方式创建一个 Task TaskCreateOptions.PreferFairness:请求 scheduler 尽量公平的执行 Task(后续文章会 .将是,Task 和线程一样,有优先级的)TaskCreateOptions.LongRunning:声明 Task 将会长时间的运行。TaskCreateOptions.AttachToParent:因为 Task 是可以嵌套的,所以这个枚举就是把一个子 task 附加到一个父 task 中。最后要提到的一点就是,我们可以在 Task 的执行体中用 Task.CurrentId 来返回 Task 的唯一表示 ID(int)。如果在 Task 执行体外使用这个属性就会得到 null。.NET 4 并行(多核)编程系列之三 从 Task 的取消 前言:因为 Task 是.NET 4 并行编程最为核心的一个类,也我们在是在并行编程常常打交道的类,所以,对 Task 对全面的了解很有必要。上篇文章主要讲述了如何创建一个 task,本篇文章主要讲述如何取消一个 task。本篇主的主要议题如下:1.1.通过轮询的方式检测 Task 是否被取消 2.2.用委托 delegate 来检测 Task 是否被取消 3.3.用 Wait Handle 还检测 Task 是否被取消 4.4.取消多个 Task 5.5.创建组合的取消 Task 的 Token 6.6.判断一个 Task 是否已经被取消了 本篇的理论不多,代码的例子很多。在 TPL 中一个标准化的操作就是”取消 Task”。之所以说它是个标准化的操作,其实是把这个操作和之前传统的多线程编程进行比较而言的。在之前的多线程编程中,我们一般是自己写一些代码来取消线程的运行。但是在.NET 4的 TPL 中就内置了取消的方法,可能我们觉得 TPL 没有必要内置这些代码,因为太简单了。但是这个内置的方法不仅仅只是取消了运行的 Task,而且还减小了在取消运行的 Task 时可能产生的一些风险,我们后续文章会详细讲述。创建一个取消的 Task 一般要进行下面一些步骤:a.a.创建 System.Threading.CancellationTokenSource 的一个实例:/create the cancellation token source CancellationTokenSource tokenSource=new CancellationTokenSource();.b.b.通过 CancellationTokenSource.Token 属性获得一个System.Threading.CancellationToken:CancellationToken token=tokenSource.Token;c.创建一个新的 Task 或者 Task,并且在构造函数传入 Action 或者Action的委托作为第一个参数,传入 CancellationToken 作为第二个参数:Task task=new Task(new Action(printMessage),token);d.d.调用 Task 的 Start()方法。上面的步骤和我们之前介绍的创建一个 Task 的代码几乎一样,只是在构造函数中多传入了一个参数。如果想要取消一个 Task 的运行,只要调用 CancellationToken 实例的Cancel()方法就可以了。有点要特别注意的,当我们调用了 Cancel()方法之后,.NET Framework 不会强制性的去关闭运行的 Task。我们自己必须去检测之前在创建 Task 时候传入的那个 CancellationToken。我们在创建 Task 是传入 CancellationToken 到构造函数,其实这个CancellationToken就是.NET Framework用来避免我们再次运行已经被取消的Task,可以说就是一个标志位。首先,进入第一个议题:1.通过轮询的方式检测 Task 是否被取消 在很多 Task 内部都包含了循环,用来处理数据。我们可以在循环中通过CancellationToken 的 IsCancellationRequest 属性来检测 task 是否被取消了。如果这个属性为 true,那么我们就得跳出循环,并且释放 task 所占用的资源(如数据库资源,文件资源等).我们也可以在 task 运行体中抛出System.Threading.OperationCanceledException 来取消运行的 task。代码如下:代码 while(true)if(token.IsCancellationRequested)/tidy up and release resources .throw new OperationCanceledException(token);else /do a unit of work 如果我们没有任何的资源要释放,那么只要简单的调用CancellationToken.ThrowIfCancellationRequested()方法,这个方法会检查是否要取消 task,并且抛出异常。代码如下:while(true)token.ThrowIfCancellationRequested();/do a unit of work 下面就给出有一个完整的例子:创建一个可以取消的 task,并且通过轮询不断的检查是否要取消 task 代码如下:代码 static void Main(string args)/create the cancellation token source CancellationTokenSource tokenSource=new CancellationTokenSource();/create the cancellation token CancellationToken token=tokenSource.Token;/create the task Task task=new Task()=for(int i=0;i for(int i=0;i Console.WriteLine(Delegate Invokedn););/wait for input before we start the task Console.WriteLine(Press enter to start task);Console.WriteLine(Press enter again to cancel task);Console.ReadLine();/start the task task.Start();./read a line from the console.Console.ReadLine();/cancel the task Console.WriteLine(Cancelling task);tokenSource.Cancel();/wait for input before exiting Console.WriteLine(Main method complete.Press enter to finish.);Console.ReadLine();3.用 Wait Handle 还检测 Task 是否被取消 第三种方法检测 task 是否被 cancel 就是调用 CancellationToken.WaitHandle属性。对于这个属性的详细使用,在后续的文章中会深入的讲述,在这里主要知道一点就行了:CancellationToken 的 WaitOne()方法会阻止 task 的运行,只有CancellationToken 的 cancel()方法被调用后,这种阻止才会释放。在下面的例子中,创建了两个 task,其中 task2 调用了 WaitOne()方法,所以 task2一直不会运行,除非调用了 CancellationToken 的 Cancel()方法,所以 WaitOne()方法也算是检测 task 是否被 cancel 的一种方法了。代码 static void Main(string args)/create the cancellation token source CancellationTokenSource tokenSource=new CancellationTokenSource();/create the cancellation token CancellationToken token=tokenSource.Token;/create the task Task task1=new Task()=for(int i=0;i /wait on the handle token.WaitHandle.WaitOne();/write out a message Console.WriteLine(Wait handle released););/wait for input before we start the task Console.WriteLine(Press enter to start task);Console.WriteLine(Press enter again to cancel task);Console.ReadLine();/start the tasks task1.Start();task2.Start();/read a line from the console.Console.ReadLine();/cancel the task Console.WriteLine(Cancelling task);tokenSource.Cancel();/wait for input before exiting Console.WriteLine(Main method complete.Press enter to finish.);Console.ReadLine();4.取消多个 Task .我们可以使用一个 CancellationToken 来创建多个不同的 Tasks,当这个CancellationToken 的 Cancel()方法调用的时候,使用了这个 token 的多个 task 都会被取消。代码 static void Main(string args)/create the cancellation token source CancellationTokenSource tokenSource=new CancellationTokenSource();/create the cancellation token CancellationToken token=tokenSource.Token;/create the tasks Task task1=new Task()=for(int i=0;i for(int i=0;i /wait until the token has been cancelled compositeSource.Token.WaitHandle.WaitOne();/throw a cancellation exception throw new OperationCanceledException(compositeSource.Token);,compositeSource.Token);/start the task .task.Start();/cancel one of the original tokens tokenSource2.Cancel();/wait for input before exiting Console.WriteLine(Main method complete.Press enter to finish.);Console.ReadLine();6.判断一个 Task 是否已经被取消了 可以使用 Task 的 IsCancelled 属性来判断 task 是否被取消了。代码如下:代码 static void Main(string args)/create the cancellation token source CancellationTokenSource tokenSource1=new CancellationTokenSource();/create the cancellation token CancellationToken token1=tokenSource1.Token;/create the first task,which we will let run fully Task task1=new Task()=for(int i=0;i .for(int i=0;i for(int i=0;i .for(int i=0;i for(int i=0;i Int32.MaxValue;i+)/put the task to sleep for 10 seconds Thread.SpinWait(10000);/print out a message Console.WriteLine(Task 1-Int value 0,i);/check for task cancellation token.ThrowIfCancellationRequested();,token);/start task task1.Start();/wait for input before exiting Console.WriteLine(Press enter to cancel token.);Console.ReadLine();/cancel the token tokenSource.Cancel();/wait for input before exiting Console.WriteLine(Main method complete.Press enter to finish.);.Console.ReadLine();代码中我们在Thread.SpinWait()方法中传入一个整数,这个整数就表示 CPU 时间片轮转的次数,至于要等待多长的时间,这个就和计算机有关了,不同的计算机,CPU 的轮转时间不一样。自旋等待的方法常常于获得同步锁,后续会讲解。使用自旋等待会一直占用 CPU,而且也会消耗 CPU 的资源,更大的问题就是这个方法会影响 Scheduler 的运作。今天就写道这里:后续文章将会逐一讲解:Task 的等待完成操作,Task 中的异常处理,获取 Task 的状态,执行 Lazily Task,常见问题解决方案