一、线程的局限
1、虽然在线程启动时不难向其中传递数据,但是当线程Join后却难以从中得到“返回值”。通常不得不创建一些共享字段(来得到“返回值”)。
2、捕获和处理线程中操作抛出的异常也是非常麻烦的。
3、在线程完成之后,就无法再次启动它,相反只能够将其Join(并阻塞当前操作线程)。
这些局限性会影响细粒度并发性的稳定实现。并会增加手动同步处理(例如使用锁、信号发送等)的依赖,而且很容易造成问题。
直接使用线程也会对性能产生影响。而且如果需要运行大量并发的I/O密集型操作,那么基于线程的方法仅仅在线程本身的开销这方面就会消耗成百上千兆的内存。
二、任务Task
1、任务简述
与线程相比,Task是一个更高级的抽象概念,它代表了一个并发操作,而该操作并不一定依赖线程来完成。
Task是可以组合(compositional)的(你可以将它们通过延续(continuation)操作串联在一起)。它们可以使用线程池减少启动延迟,也可以通过TaskCompletionSource采用回调的方式避免多个线程同时等待I/O密集型操作。
Task类型也是C#异步功能的基础类型。
2、启动任务
2.1 方法一
启动一个基于线程Task的最简单方式是使用Task.Run(Task类位于System.Threading.Tasks命名空间下)静态方法。调用时只需传入一个Action委托。
Task.Run (() => Console.WriteLine ("This is a Task"));
Task默认使用线程池中的线程,它们都是后台线程。这意味着当主线程结束时,所有的任务也会随之停止。
Task.Run会返回一个Task对象,它可以用于监控任务的执行过程。可以使用Task的Status属性来追踪其执行状态。
2.2 方法二
TaskCompletionSource可以创建一个任务,一般很少用到。
这种任务并非那种需要执行启动操作并在随后停止的任务;而是在操作结束或出错时手动创建的“附属”任务。这非常适用于I/O密集型的工作。它不但可以利用任务所有的优点(能够传递返回值、异常或延续)而且不需要在操作执行期间阻塞线程。
static async void Run()
{
var tcs = new TaskCompletionSource<bool>();
var fireAndForgetTask = Task.Delay(5000)
.ContinueWith(task => tcs.SetResult(true));
await tcs.Task;
}
3、Wait方法
调用Task的Wait方法可以阻塞当前方法,直到任务完成,这和调用线程对象的Join方法类似。
Task task = Task.Run (() =>
{
Console.WriteLine("任务开始");
Thread.Sleep (2000);
Console.WriteLine ("输出内容");
});
Console.WriteLine(task.IsCompleted); // False
task.Wait();
4、长任务
默认情况下,CLR会将任务运行在线程池线程上,这种线程非常适合执行短小的计算密集的任务。如果要执行长时间阻塞的操作(如上面的例子)则可以按照以下方式避免使用线程池线程:
Task task = Task.Factory.StartNew (() =>
{
Console.WriteLine ("线程开始了");
Thread.Sleep (2000);
Console.WriteLine ("线程输出");
}, TaskCreationOptions.LongRunning);
task.Wait(); // 等待直到线程完成
在线程池上运行一个长时间执行的任务并不会造成问题;但是如果要并行运行多个长时间运行的任务(特别是会造成阻塞的任务),则会对性能造成影响。
在这种情况下,相比于使用TaskCreationOptions.LongRunning而言,更好的方案是:
如果运行的是I/O密集型任务,则使用TaskCompletionSource和异步函数(asynchronous functions)通过回调函数而非使用线程实现并发性。
如果任务是计算密集型,则使用生产者/消费者队列可以控制这些任务造成的并发数量,避免出现线程和进程饥饿的问题。
5、返回值
Task有一个泛型子类Task<TResult>,它允许任务返回一个值。如果在调用Task.Run时传入一个Func<TResult>委托(或者兼容的Lambda表达式)替代Action就可以获得一个Task<TResult>对象。
Task<int> task = Task.Run (() => { Console.WriteLine ("任务输出"); return 3; });
//通过查询Result属性就可以获得任务的返回值。如果当前任务还没有执行完毕,则调用该属性会阻塞当前线程,直至任务结束。
int result = task.Result; // 如果没完成则阻塞
Console.WriteLine (result); // 3
另一个具有返回值的例子
Task<int> primeNumberTask = Task.Run (() =>
Enumerable.Range (2, 3000000).Count (n => Enumerable.Range (2, (int)Math.Sqrt(n)-1).All (i => n % i > 0)));
Console.WriteLine ("Task running...");
Console.WriteLine ("The answer is " + primeNumberTask.Result);
这段代码会输出“Task running...”而后在几秒钟之后输出答案216815。
6、异常处理
如果任务过程中出错,那么调用Wait()或者访问Task<TResult>的Result属性时,该异常就会被重新抛出。
Task task = Task.Run (() => { throw null; });
try
{
task.Wait();
}
catch (AggregateException aex)
{
if (aex.InnerException is NullReferenceException)
Console.WriteLine ("Null!");
else
throw;
}
7、延续
延续通常由一个回调方法实现,该方法会在操作完成之后执行。
1、调用任务的GetAwaiter方法将返回一个awaiter对象。这个对象的OnCompleted方法告知先导(antecedent)任务(primeNumberTask)当它执行完毕(或者出现错误)时调用一个委托。
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); // Writes result
});
2、另一种附加延续的方式是调用任务对象的ContinueWith方法,ContinueWith方法本身会返回一个Task对象,因此它非常适用于添加更多的延续。
Task<int> primeNumberTask = Task.Run (() =>
Enumerable.Range (2, 3000000).Count (n => Enumerable.Range (2, (int)Math.Sqrt(n)-1).All (i => n % i > 0)));
primeNumberTask.ContinueWith (antecedent =>
{
int result = antecedent.Result;
Console.WriteLine (result); // Writes 123
});