利用线程,可以方便地进行异步操作。但是线程模型有一个缺点,就是无法处理返回值。要在不同线程之间传递数据比较麻烦。任务则解决了这个问题。
完整代码在这里:https://github.com/techstay/csharp-learning-note 。
创建并执行任务
有返回值和没有返回值的任务
要创建一个没有返回值的任务,只需要向Task类的构造函数传递一个参数列表和返回值都为空的委托(也就是Action委托)即可。构造好一个Task之后,就可以调用Start方法开始执行任务,就像Thread上调用Start一样。这样任务就会开始异步地执行了。
要等待任务完成,可以调用Wait方法,这样就会在该线程上阻塞直到Task完成。另外WaitAny和WaitAll方法接受一个Task数组,WaitAny方法会阻塞调用线程直到有一个Task任务完成,而WaitAll方法会阻塞调用线程直到所有任务完成。
另外,还可以像Task的构造函数传递一个TaskCreationOptions枚举来控制Task的构造和执行。详情可以查看MSDN文档。
代码语言:javascript复制Task task1 = new Task(() => Console.WriteLine("创建一个新任务..."));
task1.Start();
task1.Wait();
Task.Run(() => Console.WriteLine("直接运行一个新任务..."))
.Wait();
要创建带有返回值的任务,需要创建一个泛型Task,并向其传入一个Function< TResult>委托。然后便可以执行任务,任务执行完成之后可以调用其Result属性查询任务执行的结果。如果此时任务还没有完成,调用Result的线程会阻塞直到任务完成。如果有异常发生,也会在这个时候抛出。
另外,由于创建一个任务并开始运行是非常常见的代码,所以Task类提供了一个Run方法,接受一个委托并立即创建并执行任务。
代码语言:javascript复制Task<int> intValue = Task.Run(() =>
{
Console.WriteLine("创建一个任务并等待结果..");
return 0;
});
Console.WriteLine($"任务的结果是:{intValue.Result}");
可以取消的任务
有时候任务运行的时间可能比较长,这个时候可能需要取消任务。这时候需要向任务传递一个CancellationToken,然后在需要取消的时候调用ThrowIfCancellationRequested方法,这样会抛出一个OperationCanceledException,并被封装到Task的AggregateException异常中并抛出。
首先来定义一个长时间运行的函数。在接到取消命令之后,任务就会被取消并抛出一个OperationCanceledException异常。这样可以区别正常运行结束的任务和非正常结束的任务。
代码语言:javascript复制internal static int SumWithLongTime(CancellationToken ct, int n)
{
int sum = 0;
for (int i = 1; i <= n; i)
{
sum = i;
if (i % 50 == 0)
Thread.Sleep(500);
ct.ThrowIfCancellationRequested();
}
return sum;
}
然后就可以执行并取消任务了。这个任务会运行大约7秒钟,在3秒钟的时候开始取消。这样会抛出一个AggregateException异常,真正引发的异常可以由GetBaseException方法获得。
代码语言:javascript复制Console.WriteLine("开始长时间的计算并取消:");
CancellationTokenSource cts = new CancellationTokenSource();
sumResult = Task.Run(() => SumWithLongTime(cts.Token, 700));
Console.WriteLine("开始计算,大约3s之后取消...");
cts.CancelAfter(3000);
try
{
Console.WriteLine($"计算结果是:{sumResult.Result}");
}
catch (AggregateException ae)
{
Console.WriteLine(ae.Message);
Console.WriteLine(ae.GetBaseException().Message);
}
执行多个任务
延续任务
当一个任务完成之后调用其Result属性,可能会阻塞调用线程直到任务完成,这样不利于资源的充分利用。这时候可以考虑使用延续任务,在一个任务完成之后启动新任务。
要使用延续任务,只需要在一个任务上调用ContinueWith方法并传递一个委托,委托的参数代表要延续的任务,可以在委托中使用参数来操作前一个任务。另外,延续任务还可以继续延续,任务内部会维护一个延续任务链。另外,还可以向ContinueWith方法传递一个TaskContinuationOptions枚举,指定延续任务的执行策略和方式。代码最后调用Wait方法防止在显示结果前退出程序。
代码语言:javascript复制Console.WriteLine("开始执行连续的任务:");
Task<int> sumResult = Task.Run(() => SumWithLongTime(500));
Task<int> otherTask = sumResult.ContinueWith(task => task.Result);
Task printTask = otherTask.ContinueWith(task => Console.WriteLine($"连续任务的结果是:{task.Result}"));
printTask.Wait();
任务和子任务
任务默认都是顶级任务,与在什么位置创建无关。但是如果在一个任务中创建了几个任务,并向其构造函数中传递了TaskCreationOptions.AttachedToParent枚举,这些任务就会变成子任务。这样一来,父任务只有在所有子任务完成之后才能完成,当然子任务也可以继续创建子任务。
代码语言:javascript复制Console.WriteLine("开始执行子任务...");
Task<int[]> parent = new Task<int[]>(() =>
{
int[] results = new int[3];
new Task(() => results[0] = SumWithLongTime(500), TaskCreationOptions.AttachedToParent).Start();
new Task(() => results[1] = SumWithLongTime(600), TaskCreationOptions.AttachedToParent).Start();
new Task(() => results[2] = SumWithLongTime(700), TaskCreationOptions.AttachedToParent).Start();
return results;
});
parent.Start();
var finished = parent.ContinueWith(parentTask => Array.ForEach(parentTask.Result, Console.WriteLine));
Console.WriteLine("正在执行子任务...");
finished.Wait();
Console.WriteLine();
任务工厂
有时候需要启动并执行多个具有相同配置的任务,这时候可以创建一个任务工厂,将相同的配置传递进任务工厂并启动任务。要创建没有返回值的任务,使用非泛型的任务工厂;要创建特定返回值类型的任务,使用泛型的任务工厂。
这里利用任务工厂改写上面的子任务的代码。
代码语言:javascript复制Console.WriteLine("开始利用工厂执行子任务...");
Task<int[]> parent = new Task<int[]>(() =>
{
int[] results = new int[3];
TaskFactory factory = new TaskFactory(CancellationToken.None
, TaskCreationOptions.AttachedToParent
, TaskContinuationOptions.AttachedToParent
, TaskScheduler.Current);
factory.StartNew(() => results[0] = SumWithLongTime(500));
factory.StartNew(() => results[1] = SumWithLongTime(600));
factory.StartNew(() => results[2] = SumWithLongTime(700));
return results;
});
parent.Start();
var finished = parent.ContinueWith(parentTask => Array.ForEach(parentTask.Result, Console.WriteLine));
Console.WriteLine("正在执行子任务...");
finished.Wait();
Console.WriteLine();