您的位置:首页 > 编程语言 > PHP开发

.NET4.0并行计算技术基础(8) 推荐

2009-10-12 11:19 423 查看
[align=left] [/align]
[align=left]说明:[/align]
[align=left] [/align]
[align=left]要想看懂本系列文章,需要您对.NET多线程开发有基本的了解。我在新书《面向对象的艺术 ——.NET Framework 4.0技术剖析与应用》(暂名)中花了近200页的篇幅来介绍.NET多线程开发技术,可帮助大家循序渐进地掌握技术,呵呵,在此先作个广告。[/align]
今天贴出第8讲 “任务的同步”,本章内容过半了。
金旭亮
2009.10.12
===========================================
[align=center] [/align]
[align=center] [/align]
[align=center].NET4.0并行计算技术基础(8)[/align]

这是一个系列讲座,前面几讲的链接为:

.NET 4.0 并行计算技术基础(1)

.NET 4.0 并行计算技术基础(2)
.NET 4.0并行计算技术基础(3)
.NET4.0并行计算技术基础(4)
.NET4.0并行计算技术基础(5)
.NET 4.0并行计算技术基础(6)
.NET4.0并行计算技术基础(7)

====================================================

19.3.6 同步多个任务

在并行计算应用程序中,通常会创建多个Task对象以执行不同的工作任务,而依据具体应用场景,这些工作任务对象之间又会有着相互协作的需求,比如可能要求某个工作任务完成以后自动启动一个或多个新的Task对象执行后继处理工作,或者某个正在执行的工作任务中途需要等待另一个工作任务执行完毕才能执行,这就是任务的同步问题。
Task类提供了ContinueWith和Wait系列方法,在“任务”的层次(而不是线程的层次)实现任务的同步。

1 使用ContinueWith

例如,以下代码在task1完成之后自动运行task2:

Task task1=new Task(()=>
{
DoStep1();
});
Task task2 = task1.ContinueWith[/b]((PrevTask) =>
{
DoStep2();
});

task1.Start();

上述代码中的PrevTask参数代表已完成的“前辈”Task对象,TPL会将此对象传给后继的Task对象,因此,在后继对象中可以通过此参数获取前一个任务的相关信息。
ContinueWith()方法有多个重载形式,其中很有用的是返回一个Task<TResult>的重载形式:

public Task<TResult> ContinueWith<TResult>(
Func<Task, Task<TResult>> continuationFunction);

当任务需要返回一个唯一值时,可以使用这个重载形式。
有的ContinueWith()方法重载形式接收一个TaskContinuationOptions类型的参数:

public Task ContinueWith(
Action<Task> continuationAction,
TaskContinuationOptions continuationOptions);

TaskContinuationOptions是一个枚举,可以使用它来指定“在何种情况下”才执行后继的工作任务。例如,以下代码指定只有task1中有未捕获的异常时,才运行task2:

Task task2 = task1.ContinueWith((PrevTask) =>
{
DoWithException();
},TaskContinuationOptions.OnlyOnFaulted);

2 使用Wait系列方法

另一个被广泛使用的任务同步手段是Task类的Wait系列方法,此系列方法可分为3类,每类方法又有着多个重载形式。
1. Wait():等待单个任务的完成
2. WaitAll():等待一组任务的全部完成
3. WaitAny():等待一组任务的任何一个任务完成

当并行程序调用上述方法时,会在调用线程上阻塞等待任务完成。
以下代码等待单个任务的完成:

Task t = Task.Create(...);
...
t.Wait();

以下代码等待多个任务的完成:
[align=left] [/align]
Task t1 = Task.Create(...);
Task t2 = Task.Create(...);
Task t3 = Task.Create(...);
...
Task.WaitAll(t1, t2, t3);

将上述代码中的WaitAll改为WaitAny,则t1,t2,t3中任何一个完成时,当前任务都会结束等待状态而继续执行。

3 创建父子类型的任务

当一个任务会创建另一个任务时,称此任务为“父任务”,被创建的为“子任务”。使用任务之间的“父子关系”,可以实现类似于Task.ContinueWith的功能:

Task tskParent = new Task(() =>
{
//父任务完成的工作
//创建后继子任务并自动启动
Task.Factory.StartNew(() => MethodA());
Task.Factory.StartNew(() => MethodB());
Task.Factory.StartNew(() => MethodC());

});
//启动父任务
tskParent.Start();
//等待整个任务的完成
tskParent.Wait();

与使用Task.ContinueWith相比,使用父子任务的好处在于可以减少应用程序需要跟踪状态的任务对象个数。

4 非阻塞方式等待

不管是使用ContinueWith还是Wait系列方法,调用这些方法的线程都会阻塞等待。如果不希望阻塞当前线程,可以通过轮询Task对象的IsCompleted属性来了解其是否完成,以下是框架代码:

while (!task1.IsCompleted)
{
Thread.SpinWait(10000000); //让当前线程时刻盯着前一任务的完成状态
//可以安排进行其它工作
}
//task1已完成,进行后继工作……

5 小结

本节介绍的任务同步的方法都很简单,但通过灵活的组合,可以实现复杂的并行计算任务,而同样的功能,如果直接使用线程来处理,其工作量会增加很多。这也是使用任务并行库优越性的表现之一。

19.3.7 任务处理结果的取回

任何一个并行计算程序都需要处理一定量的数据,因此,需要解决如何将数据在任务中传送并在合适的时候取回处理结果的问题。

1 使用线程同步手段取回数据处理结果

我们使用Task对象来代表一个并行处理任务,并调用其Start()方法启动并行处理过程。从Task类的构造函数可以看到,每个Task类关联的是一个Action或Action<T>委托,其所引用的函数其返回值为void,很明显此函数无法直接将处理结果返回给任务的启动者。
然而,由于任务的执行是由线程负责的,所以,可以在任务函数中直接访问程序中的用于保存处理结果的共享资源(比如某个对象的公有属性,或者类的静态成员等)。但这时,必须使用一种线程同步手段来通知任务启动者线程本线程工作结束,从而启动任务的线程可以从共享资源中取回处理结果。
请看示例程序GetResultFromTask。这个程序在内部使用了一个ManualResetEvent对象,主线程在启动处理工作后等待此对象变为Signaled状态。示例程序通过Task对象启动一个数组求和计算任务,在其任务函数中将处理结果保存到一个共享的静态字段中,然后ManualResetEvent.Set()方法通知主线程可以取出处理结果。
这种方法虽然可行,但本质上还是基于“线程”的开发,TPL的优势没有显示出来。

2 使用TPL提供的任务同步手段取回处理结果

我们完全可以不用“原始”的线程同步对象,而直接使用TPL所提供的任务同步机制达到同样的目的。
请看示例项目GetResultFromTaskWithoutThreadSync。它与前一个示例项目GetResultFromTask功能完成一样,但它利用到了Task对象的ContinueWith()方法:

public Task ContinueWith(Action<Task> continuationAction);

注意这是一个实例方法,对它的调用将返回一个新的Task对象,这个对象引用的任务函数由方法参数提供。
这一方法的功能是:
当前Task对象的任务函数执行完毕后,自动启动其ContinueWith()方法所创建的“后继”Task对象。当前“已完成”的Task对象将作为“后继”Task对象任务函数的参数传入。
下面列出示例的代码框架,详细代码请直接看项目源码:

static void Main(string[] args)
{
//需要并行执行的数据处理函数
Action<object> ProcessData = delegate(object end)
{
//编写代码完成数据处理工作...
//保存处理结果...
};
//用于取回处理结果的函数
Action<Task> GetResult=delegate(Task finishedTask)
{
if(finishedTask.IsFaulted)
Console.WriteLine("任务在执行时发生异常:",
finishedTask.Exception.Message);
else
Console.Write("程序运行结果为{0}", Program.result);
};
//创建并行处理数据的任务对象
Task tskProcess = new Task(ProcessData, 1000000);
//当数据处理结束时,自动启动下一个工作任务,取回上一任务的处理结果
Task tskGetResult = tskProcess.ContinueWith(GetResult);
//开始并行处理数据……
tskProcess.Start();
Console.ReadKey();
}

上述代码非常简明,值得注意的是在后继的任务中可以通过检测“已完成的”Task对象的IsFaulted属性得知是否在其处理过程出现了异常。
通过连续使用ContinueWith()方法,我们可以启动一连串的“首尾”相接的任务,因此特别适合并行执行多个任务,每个任务内部又包容着一系列需要顺序执行的子任务。
有关任务之间相互协作与配合(称为“任务同步”)的问题,后面章节中还有介绍。

3 使用Task<TResult>得到任务处理结果

与Task不一样,Task<TResult>类直接关联一个可以有返回值的函数。以下是它的一个构造函数:

public Task(Func<object, TResult> function, object state);

从这个构造函数可以看出,Task<TResult>对象关联着一个任务函数(由function参数引用此函数),构造函数中的object参数将成为任务函数的实参。
Task<TResult>类提供了一个Result属性用于取回任务函数的执行结果:

public TResult Result { get; internal set; }

可以直接使用new关键字创建Task<TResult>类的实例,然后手动调用其Start()方法启动,也可以通过工厂类TaskFactory<TResult>的StartNew()方法一步完成创建任务对象和启动运行的工作。Task<TResult>类提供了以下静态属性引用工厂类:

public static TaskFactory<TResult> Factory { get; }

示例程序GetResultFromTaskTResult展示了Task<TResult>类的用法,其代码框架如下:

static void Main(string[] args)
{
//完成数据处理工作,结果将作为函数返回值
Func<object,long> del = delegate(object end)
{
long result= 0;
//...数据处理代码略
return result;
};
Task<long> tsk = new Task<long>(del, 1000000);
//启动运行
tsk.Start();
//取回结果
Console.Write("程序运行结果为{0}", tsk.Result);
Console.ReadKey();
}

这里比较有趣的是整个代码中没有一句线程同步代码。您可能会奇怪,主线程与工作线程的同步是怎么实现的?主线程如何知道工作线程承担的工作任务已处理结束?
答案就在Task<TResult>类的Result属性中。请注意,Task<TResult>类派生自Task类,而此Task类提供了一个Wait()方法用于等待工作任务的执行结束,所以,在访问Task<TResult>类的Result属性时,如果工作任务还未执行完毕,则尝试取回结果的线程会阻塞等待,因为Result属性的get访问器函数一直没有返回,请看以下框架代码:

public TResult Result
{
get
{
if (!base.IsCompleted)
{
base.Wait(); //阻塞等待任务的执行结束
}
//……
}
internal set
{
//……
}
}

上述代码另一个要注意的地方是Result属性的Set访问器是internal的,这意味着应用程序不能直接设置其值。

4 小结

本小节介绍了3种典型的取回任务处理结果的基本方法,很明显,第一种方式需要显式地使用线程同步手段,不是一个好的方案,第二种方式使用Task<TResult>,使用简单,最适合于取回单个[/b]工作任务的单个[/b]处理结果,对于内部包含多个并行执行子任务的复杂工作任务,使用Task.ContinueWith()最合适,它可以直接处理多个数据处理结果,并且在很大程度上减少了显式编码锁定共享资源的需求。

=========================================

下一讲:介绍如何取消一个工作任务

请看《.NET4.0并行计算技术基础(9)

附件:http://down.51cto.com/data/2354281
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息