.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("HelloWorld");); 我们用静态方法: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交给底下的线程去执行。 下面就看看代码:代码usingSystem;usingSystem.Threading.Tasks;namespaceListing_02classListing_02staticvoidMain(stringargs)/useanActiondelegateandanamedmethodTasktask1=newTask(newAction(printMessage);/useaanonymousdelegateTasktask2=newTask(delegateprintMessage(););/usealambdaexpressionandanamedmethodTasktask3=newTask()=>printMessage();/usealambdaexpressionandananonymousmethodTasktask4=newTask()=>printMessage(););task1.Start();task2.Start();task3.Start();task4.Start();/waitforinputbeforeexitingConsole.WriteLine("Mainmethodcomplete.Pressentertofinish.");Console.ReadLine();staticvoidprintMessage()Console.WriteLine("HelloWorld"); 不知道大家注意到了没有,上面代码创建Task的方法和我们之前的第一段代码的创建Task的方法不同。在之前我们采用的是Task.Factory.StartNew()方法来创建的,这个方法创建Task并且开始运行Task,其实两端代码的结果是一样的,这里给出一点建议:如果这是想简单的创建一个Task,那么使用Factory.NewStart()来创建,很简便,如果像对所创建的Task附加更多的定制和设置特定的属性,那么还是得一步一步的按照我们说的那些步骤来。(详细的我们后续会介绍的) 2.1 为创建的Task传入参数 我们之前提过,在创建Task的时候,我们在构造函数中传入了一个System.Action的委托,如果我们想要把一些参数传入到Task中,那么我 们可以传入System.Action<object>的委托,其中的那个object就是我们传入的参数。还是给大家举个例子:代码usingSystem;usingSystem.Threading.Tasks;namespaceListing_04classListing_04staticvoidMain(stringargs)stringmessages="Firsttask","Secondtask","Thirdtask","Fourthtask"foreach(stringmsginmessages)TaskmyTask=newTask(obj=>printMessage(string)obj),msg);myTask.Start();/waitforinputbeforeexitingConsole.WriteLine("Mainmethodcomplete.Pressentertofinish.");Console.ReadLine();staticvoidprintMessage(stringmessage)Console.WriteLine("Message:0",message); 注意:我们在传入参数后,必须把参数转换为它们原来的类型,然后再去调用相应的方法。例子中,因为System.Action对应的方法是printMessage()方法,而这个方法的要求的参数类型是string,所以要转换为string。想向Task传入参素,只能用System.Action<object>3.获取Task的执行结果 如果要获取Task的结果,那么在创建Task的时候,就要采用Task<T>来实例化一个Task,其中的那个T就是task执行完成之后返回结果的类型。之后采用Task实例的Result属性就可以获取结果。 代码显示如下: 代码staticvoidMain(stringargs)/createthetaskTask<int>task1=newTask<int>()=>intsum=0;for(inti=0;i<100;i+)sum+=i;returnsum;);task1.Start();/writeouttheresultConsole.WriteLine("Result1:0",task1.Result);Console.ReadLine(); 只有在task执行完成之后,才能获取到Result的值。 下面的代码展示了如何通过Task.Factory.StartNew<T>()创建一个Task,并且获取结果: 代码staticvoidMain(stringargs)/createthetaskTask<int>task1=Task.Factory.StartNew<int>()=>intsum=0;for(inti=0;i<100;i+)sum+=i;returnsum;);/writeouttheresultConsole.WriteLine("Result1: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.取消多个Task5.5.创建组合的取消Task的Token6.6. 判断一个Task是否已经被取消了本篇的理论不多,代码的例子很多。在TPL中一个标准化的操作就是”取消Task”。之所以说它是个标准化的操作,其实是把这个操作和之前传统的多线程编程进行比较而言的。在之前的多线程编程中,我们一般是自己写一些代码来取消线程的运行。但是在.NET 4的TPL中就内置了取消的方法,可能我们觉得TPL没有必要内置这些代码,因为太简单了。但是这个内置的方法不仅仅只是取消了运行的Task,而且还减小了在取消运行的Task时可能产生的一些风险,我们后续文章会详细讲述。创建一个取消的Task一般要进行下面一些步骤:a.a.创建System.Threading.CancellationTokenSource的一个实例:/createthecancellationtokensourceCancellationTokenSourcetokenSource=newCancellationTokenSource();b.b.通过CancellationTokenSource.Token属性获得一个System.Threading.CancellationToken:CancellationTokentoken=tokenSource.Token;c.创建一个新的Task或者Task<T>,并且在构造函数传入Action或者Action<object>的委托作为第一个参数,传入CancellationToken作为第二个参数: Tasktask=newTask(newAction(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)/tidyupandreleaseresourcesthrownewOperationCanceledException(token);else/doaunitofwork如果我们没有任何的资源要释放,那么只要简单的调用CancellationToken.ThrowIfCancellationRequested()方法,这个方法会检查是否要取消task,并且抛出异常。代码如下:while(true)token.ThrowIfCancellationRequested();/doaunitofwork下面就给出有一个完整的例子:创建一个可以取消的task,并且通过轮询不断的检查是否要取消task代码如下:代码staticvoidMain(stringargs)/createthecancellationtokensourceCancellationTokenSourcetokenSource=newCancellationTokenSource();/createthecancellationtokenCancellationTokentoken=tokenSource.Token;/createthetaskTasktask=newTask()=>for(inti=0;i<int.MaxValue;i+)if(token.IsCancellationRequested)Console.WriteLine("Taskcanceldetected");thrownewOperationCanceledException(token);elseConsole.WriteLine("Intvalue0",i);,token);/waitforinputbeforewestartthetaskConsole.WriteLine("Pressentertostarttask");Console.WriteLine("Pressenteragaintocanceltask");Console.ReadLine();/startthetasktask.Start();/readalinefromtheconsole.Console.ReadLine();/cancelthetaskConsole.WriteLine("Cancellingtask");tokenSource.Cancel();/waitforinputbeforeexitingConsole.WriteLine("Mainmethodcomplete.Pressentertofinish.");Console.ReadLine();2.用委托delegate来检测Task是否被取消我们可以在注册一个委托到CancellationToken中,这个委托的方法在CancellationToken.Cancel()调用之前被调用。我们可以用这个委托中的方法来作为一个检测task是否被取消的另外一个可选的方法,因为这个方法是在Cancel()方法被调用之前就调用的,所以这个委托中的方法可以检测task是否被cancel了,也就是说,只要这个委托的方法被调用,那么就说这个CancellationToken.Cancel()方法被调用了,而且在这个委托的方法中我们可以做很多的事情,如通知用户取消操作发生了。下面的代码给出了一个例子。代码staticvoidMain(stringargs)/createthecancellationtokensourceCancellationTokenSourcetokenSource=newCancellationTokenSource();/createthecancellationtokenCancellationTokentoken=tokenSource.Token;/createthetaskTasktask=newTask()=>for(inti=0;i<int.MaxValue;i+)if(token.IsCancellationRequested)Console.WriteLine("Taskcanceldetected");thrownewOperationCanceledException(token);elseConsole.WriteLine("Intvalue0",i);,token);/registeracancellationdelegatetoken.Register()=>Console.WriteLine(">>>>>>DelegateInvokedn"););/waitforinputbeforewestartthetaskConsole.WriteLine("Pressentertostarttask");Console.WriteLine("Pressenteragaintocanceltask");Console.ReadLine();/startthetasktask.Start();/readalinefromtheconsole.Console.ReadLine();/cancelthetaskConsole.WriteLine("Cancellingtask");tokenSource.Cancel();/waitforinputbeforeexitingConsole.WriteLine("Mainmethodcomplete.Pressentertofinish.");Console.ReadLine();3.用Wait Handle还检测Task是否被取消第三种方法检测task是否被cancel就是调用CancellationToken.WaitHandle属性。对于这个属性的详细使用,在后续的文章中会深入的讲述,在这里主要知道一点就行了:CancellationToken的WaitOne()方法会阻止task的运行,只有CancellationToken的cancel()方法被调用后,这种阻止才会释放。在下面的例子中,创建了两个task,其中task2调用了WaitOne()方法,所以task2一直不会运行,除非调用了CancellationToken的Cancel()方法,所以WaitOne()方法也算是检测task是否被cancel的一种方法了。代码staticvoidMain(stringargs)/createthecancellationtokensourceCancellationTokenSourcetokenSource=newCancellationTokenSource();/createthecancellationtokenCancellationTokentoken=tokenSource.Token;/createthetaskTasktask1=newTask()=>for(inti=0;i<int.MaxValue;i+)if(token.IsCancellationRequested)Console.WriteLine("Taskcanceldetected");thrownewOperationCanceledException(token);elseConsole.WriteLine("Intvalue0",i);,token);/createasecondtaskthatwillusethewaithandleTasktask2=newTask()=>/waitonthehandletoken.WaitHandle.WaitOne();/writeoutamessageConsole.WriteLine(">>>>>Waithandlereleased"););/waitforinputbeforewestartthetaskConsole.WriteLine("Pressentertostarttask");