C#线程:任务Task

Task是一个更高级的抽象概念,它代表了一个并发操作,而该操作并不一定依赖线程来完成。Task是可以组合的(可以将它们通过延续(continuation)操作串联在一起)。它们可以使用线程池减少启动延迟,也可以通过TaskCompletionSource采用回调的方式避免多个线程同时等待I/O密集型操作。

Task类是Framework 4.0时作为并行编程库的组成部分引入的。然而它们后来经历了许多改进(通过使用等待器(awaiter)),从而在常见的并发场景中发挥了越来越大的作用。Task类也是C#异步功能的基础类型。

1.启动任务

启动一个基于线程的Task的最简单方式是使用Task.Run(Task类位于System.Threading.Tasks命名空间)静态方法。调用时只需传入一个Action委托:

Task.Run(()=> Console.WriteLine("foo"));

和下面的方式很相似:

new Thread (()=> Console.WriteLine("foo")).Start();

Task.Run会返回一个Task对象,它可以用于监控任务的执行过程。这一点与Thread对象不同。我们可以使用Task的Status属性来追踪其执行状态。

Wait方法

调用Task的Wait方法可以阻塞当前方法,直到任务完成,这和调用线程对象的Join方法类似:

Task task = Task.Run (() =>
{
    Console.WriteLine ("Task started");
    Thread.Sleep (2000);
    Console.WriteLine ("Foo");
});
Console.WriteLine (task.IsCompleted);  // False
task.Wait();

可以在Wait中指定一个超时时间和取消令牌(可选)来提前终止等待状态。

长任务

默认情况下,CLR会将任务运行在线程池线程上,这种线程非常适合执行短小的计算密集的任务。如果要执行长时间阻塞的操作(如上面的例子),则可以按照以下方式避免使用线程池线程:

Task task = Task.Factory.StartNew(()=>......,TaskCreationOptions.LongRunning);

2.返回值

Task有一个泛型子类Task<TResult>,它允许任务返回一个值。如果在调用Task.Run时传入一个Func<TResult>委托(或者兼容的Lambda表达式)替代Action就可以获得一个Task<TResult>对象,通过查询Result属性就可以获得任务的返回值。如果当前任务还没有执行完毕,调用该属性会阻塞当前线程,直至任务结束。

Task<int> task = Task.Run(() =>
{
    Console.WriteLine("Foo");
    return 3;
});
int result = task.Result; 

可以将Task理解为一个“未来值”,它封装了Result并将在以后生效。

3.异常

任务可以方便地传播异常,这和线程是截然不同的。因此,如果任务中的代码抛出一个未处理异常,那么调用Wait()或者访问Task的Result属性时,该异常就会被重新抛出。

Task task = Task.Run (() => { throw null; });
try 
{
    task.Wait();
}
catch (AggregateException aex)
{
    if (aex.InnerException is NullReferenceException)
        Console.WriteLine ("Null!");
    else
        throw;
}

使用Task的IsFaulted和IsCanceled属性可以在不抛出异常的情况下检测出错的任务。如果IsCanceled为true,则说明任务抛出了OperationCanceledException;如果IsFaulted为true,则说明任务抛出了其他类型的异常,通过Exception属性可以了解该异常的信息。

4.延续

延续会告知任务在完成后继续执行后续的操作。延续通常由回调方法实现,该方法会在操作完成后执行。
以下是计算素数的例子:

Task<int> primeNumberTask = Task.Run (() =>
    Enumerable.Range (2, 3000000).Count (n => 
        Enumerable.Range (2, (int)Math.Sqrt(n)-1).All (i => n % i > 0)));

var awaiter = primeNumberTask.GetAwaiter();
awaiter.OnCompleted (() => 
{
    int result = awaiter.GetResult();
    Console.WriteLine (result);      
});

调用任务的GetAwaiter方法将返回一个awaiter对象。这个对象的OnCompleted方法告知先导任务(primeNumberTask)当它执行完毕(或者出现错误)时调用一个委托。将延续附加到一个已执行完毕的任务上是完全没有问题的,此时,延续的逻辑将会立即执行。

如果先导任务出现错误,则延续代码调用awaiter.GetResult()时会重新抛出异常。对于非泛型任务,GetResult的返回值为void,这个函数的用途完全是为了重新抛出异常。

5.TaskCompletionSource类

另一种创建任务的方法是使用TaskCompletionSource,这种任务并非那种需要执行启动操作并在随后停止的任务;而是在操作结束或出错时手动创建的“附属”任务。这非常适用于I/O密集型的工作:它不但可以利用任务所有的优点(能够传递返回值、异常或延续),而且不需要在操作执行期间阻塞线程。

TaskCompletionSource的用法很简单,直接进行实例化即可。它包含一个Task属性,返回一个Task对象。
下面的例子会在等待5秒钟后输出42:

var tcs = new TaskCompletionSource<int>();
new Thread (() => 
{ 
    Thread.Sleep (5000); 
    tcs.SetResult (42); 
}).Start();

Task<int> task = tcs.Task;      
Console.WriteLine (task.Result);

6.Task.Delay方法

它是Task类的一个静态方法,是Thread.Sleep的异步版本,不会造成阻塞。

posted @ 2022-09-13 15:21  一纸年华  阅读(1811)  评论(0编辑  收藏  举报