使用并行处理执行多任务处理

在应用程序中执行多任务处理主要出于以下原因:
1.增强可响应性
长时间运行的操作可能涉及不需要处理器事件的任务。比如读写本地硬盘或通过网络收发数据。这个时候让CPU空转来等待任务完成没有意义。这个时候完全可以去干别的事情。
2.增强可伸缩性
如一个操作是CPU限制的,可有效利用可用的处理资源,并利用这些资源减少执行操作所需的事件来增强伸缩性.

在多核处理器之前的时候,单线程应用程序在一个更快的处理器上运行,速度就能变得更快.但在多核处理器的时代,在相同时钟频率的单核、双核或四核处理器上,单线程应用程序的速度是没有任何变化。
区别在于:在双核处理器上,一个内核处于空闲状态;四核处理器上,三个会处于空闲状态.
要最大化利用多核处理器,必须在写程序时就想好怎么利用多任务处理.

用.NET Framewordk实现多任务处理

多任务处理是指同时做多件事情的能力.理想情况下,多核处理器上运行的应用程序应执行跟处理内核数量一样多的并发任务,让每个内核都工作起来.
但是需要考虑以下几个问题:
1.如何将应用程序分解成一组并发操作?
2.如何安排一组操作在多个处理器上并发执行?
3.如何保证只执行处理器数量那么多的并发操作?
4.如一个操作阻塞(比如等待I/O操作完成),如何检测这种情况,并安排处理执行另一个操作,而不是在那傻等?
5.如何知道一个或多个并发操作已完成?

关于这些问题,在C#提供了Task类以及一套相关的类型来解决这些问题


任务、线程和线程池

Task类是对一个并发操作的抽象。要创建Task对象来运行一个代码块,可实例化多个Task对象.然后,如果有足够数量的处理器,就可以让它们并发运行.

如果构建传统的桌面应用程序,可直接在代码中使用System.Threading命名空间中的Thread类,但Thread类在UWP应用中不可用,要改为使用Task类.

Task类对线程处理进行了强大抽象,使你可以简单区分应用程序的并行度并行单位.
在单核处理器上,这两个通常没区别。但在多核处理器上却不一样.
如果直接依赖线程设计程序,程序会使用你显式创建的那些数量的线程,操作系统只调用那些数量的线程。如果线程数量显著超过可用处理器的数量,会造成CPU过载以及较差的响应能力. 相反,则会造成CPU欠载,大量处理能力被白白浪费了

创建、运行和控制任务

可用Task构造器创建Task对象,Task构造器有多个重载版本,所有版本都要求提供一个Action委托作为参数,Action委托引用的是不返回值的方法.
C#任务、线程、Task类、Parall类
C#任务、线程、Task类、Parall类
由于经常要创建和运行任务,Task类提供了静态Run方法来合并两个操作:
C#任务、线程、Task类、Parall类
Run方法获取一个指定了要执行的操作的Action委托,然后立即开始任务,并返回对Task对象的引用.
任务运行的方法结束,任务结束,运行任务的线程返回线程池,以便执行另一个任务.

我们可以通过使用延续,安排在一个任务结束后执行另一个任务.
比如:
C#任务、线程、Task类、Parall类
延续用Task对象的ContinueWith方法创建.一个Task对象的操作完成后,调度器自动创建新Task对象来运行由ContinueWith方法指定的操作
注意:延续所指定的方法需要获取一个Task参数:
C#任务、线程、Task类、Parall类
调度器向方法传递对已完成任务的引用.ContinueWith返回一个新的Task对象的引用.ContinueWith有许多重载版本,可以通过官网进行更详细的了解

执行并行操作的应用程序需要对任务进行同步。
注意:同步指的是一个操作开始后必须等待它完成;异步则是不用等它完成,可以立即返回做其他事情.不要将同步理解成同时。
Task类提供Wait方法来实现简单的任务协作机制。
它允许暂停当前线程,直至指定任务完成.
C#任务、线程、Task类、Parall类
可以用Task类的静态WaitAll和WaitAny方法等待一组方法:
C#任务、线程、Task类、Parall类
C#任务、线程、Task类、Parall类

使用Parallel类对任务进行抽象

Parallel类允许对常见编程构造进行并行化,同时不要求重新设计应用程序.在内部,Paraller类会创建它自己的一组Task对象,并在这些任务完成时自动同步.
例子如下:
C#任务、线程、Task类、Parall类
Parallel.For 在它定义的循环中,迭代可用任务来并行处理。用法有点类似for循环,要指定起始值和结束值.
C#任务、线程、Task类、Parall类
Parallel.Invoke以并行任务的形式执行一组无参方法.
要指定无参且无返回值的一组委托方法调用.每个方法调用都可以在单独的线程上运行。
并行度由Parallel类根据当前环境和当前的工作负荷决定,比如如果用Parallel.For实现迭代1000次的循环,并非一定创建1000个并发的任务(除非你的处理器有1000个内核)。.NET会创建认为最佳数量的任务。一个任务可能执行多次迭代,任务相互写作来决定每个任务执行哪些迭代。因此,不能对迭代执行的顺序做出任何假设。必须确保迭代和迭代之间没有依赖性.

什么时候不要使用Parallel类

如果代码不是CPU限制的,并行化就不一定能提升性能。创建任务、在单独线程上运行任务以及等待任务完成的开销有可能大于直接运行该方法的开销。
一般只有在绝对必要时才使用Parallel.Invoke。只有计算密集型的操作才需要Parallel.Invoke.
使用Parallel类的另一个前提是并行操作必须独立.如果迭代相互之间有依赖,就不适合用Parallel.For来并行化.
C#任务、线程、Task类、Parall类
C#任务、线程、Task类、Parall类
可以看到两次结果竟然不一样的。
因为无法保证创建的各个任务按固定顺序调用test方法。而且代码不是线程安全的,因为多个线程可能尝试同时修改i变量。
所以一般只有保证循环的每一次迭代都可以独立进行,才可以使用Parallel.For.而且要对代码进行全面测试.

相关文章: