线程(Thread)时用来创建并发(concurrency)的一种低级别工具,它有一些限制,尤其是:
所以很难使用较小的并发(concurrent)来组建大型的并发,导致了对手动同步的更大依赖以及随之而来的问题。
而 Task 类可以很好的解决上述问题。
Task 是一个相对高级的抽象,它代表了一个并发操作(concurrent),该操作可能由 Thread 支持,或不由 Thread 支持。
Task 类在 System.Threading.Tasks 命名空间下
开始一个 Task 最简单的办法就是使用 Task.Run(.Net 4.5 添加的,.Net 4.0 的使用是 Task.Factory.StartNew)这个静态方法:
使用时传入一个 Action 委托即可
public static void Main(string[] args)
{
Task.Run(() => Console.WriteLine("Foo"));
}
Task 默认使用线程池,也就是后台线程。当主线程结束时,创建的所有的 tasks 都会结束。可通过阻塞主线程的方法,来使创建的 tasks 完成执行。
public static void Main(string[] args)
{
Task.Run(() => Console.WriteLine("Foo"));
Console.ReadLine();
}
Task.Run 返回一个 Task 对象,可以使用它来监视其过程。
在 Task.Run 之后,没有调用 Start 方法,因为通过 Task.Run 创建的使“热”任务(host task),即创建完之后就准备开始运行,不需要调用 Start 方法。
可以通过 Task 的 Status 属性来跟踪 task 的执行状态。
调用 task 的 Wait 方法会进行阻塞直到操作完成,相当于调用 Thread 的 Join 方法。
public static void Main(string[] args)
{
Task task = Task.Run(() =>
{
Thread.Sleep(3000);
Console.WriteLine("Foo");
});
Console.WriteLine(task.IsCompleted); //false
task.Wait(); //阻塞,直到 task 完成操作
Console.WriteLine(task.IsCompleted); //true
}
Wait 方法也可以指定一个超时时间和一个取消令牌来提前结束等待。
默认情况下,CLR 在线程池中运行 Task,这非常适合短时间运行的 Compute-Bound 类工作。
针对长时间运行的任务或者阻塞操作,可以不采用线程池的方法。
public static void Main(string[] args)
{
Task task = Task.Factory.StartNew(() =>
{
Thread.Sleep(3000);
System.Console.WriteLine("Foo");
}, TaskCreationOptions.LongRunning);
}
如果同时运行多个 long-running tasks(尤其是其中由处于阻塞状态的),那么性能将会受到很大影响,这时由比 TaskCreationOptions.LongRunning 更好的办法:
Task 有一个泛型子类叫做 Task
使用 Func
随后,可以通过 Result 属性来获得返回的结果。如果,这个 task 还没有完成操作,访问 Result 属性会阻塞该线程直到该 task 完成操作。
public static void Main(string[] args)
{
Task<int> task = Task.Run(() =>
{
System.Console.WriteLine("Foo");
return 3;
});
int result = task.Result; //如果 task 没完成,那么就阻塞
Console.WriteLine(result);
}
Task
Task 与 Thread 不一样,Task 可以很方便的传播异常。
如果 task 里面抛出了一个未处理的异常(故障),那么该异常就会重新抛出给:
调用了 wait() 的地方
访问了 Task
public static void Main(string[] args)
{
Task task = Task.Run(() => { throw null; });
try
{
task.Wait(); //异常抛出到此处
}
catch(AggregateException aex)
{
if(aex.InnerException is NullReferenceException)
{
Console.WriteLine("Null");
}
else
{
throw;
}
}
}
输出结果:
Null
CLR 将异常包裹在 AggregateException 里,一边在并行编程场景中发挥很好的作用。
当无需抛出异常时,通过 Task 的 IsFaulted 和 IsCanceled 属性也可以检测出 Task 是否发生了故障:
一个 Continuation 会对 Task 说:“当你结束的时候,继续做点其它的事。”
Continuation 通常是通过回调的方式实现的:
当操作一结束,就开始执行
public static void Main(string[] args)
{
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);
});
Console.ReadLine(); //阻塞应用进程
}
在 task 上调用 GetAwaiter 会返回一个 awaiter 对象。它的 OnCompleted 方法会告诉之前的 task:“当你结束/发生故障的时候要执行委托”。
可以将 Continuation 附加到已经结束的 task 上面,此时 Continuation 将会被安排立即执行。
任何可以暴露下列两个方法和一个属性的对象就是 awaiter:
awaiter没有接口或者父类来统一这些成员。其中 OnCompleted 是 INotifyCompletion 接口的一部分。
如果之前的任务发生故障
针对非泛型的 task,GetResult() 方法有一个 void 返回值,它仅用来重新抛出异常。
如果同步上下文出现了,那么 OnCompleted 会自动捕获它,并将 Continuation 提交到这个上下文中。这一点在富客户端应用中非常有用,因为它会把 Continuation 放回到 UI 线程中。
如果是编写的一个库,则不希望出现上述行为,因为开销较大的 UI 线程切换应该在程序运行离开库的时候只发生一次,而不是出现在方法调用之间。所以,我们可以使用 ConfigureAwait 方法来避免这种行为。
public static void Main(string[] args)
{
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.ConfigureAwait(false).GetAwaiter();
awaiter.OnCompleted(() =>
{
int result = awaiter.GetResult();
Console.WriteLine(result);
});
Console.ReadLine(); //阻塞应用进程
}
如果没有同步上下文出现,或者使用的是 ConfigureAwait(false),那么 Continuation 会运行在先前 task 的同一个线程上,从而避免不必要的开销。
另外一种附件 Continuation 的方式就是调用 task 的 ContinueWith 方法
public static void Main(string[] args)
{
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(task =>
{
int result = awaiter.GetResult();
Console.WriteLine(result);
});
Console.ReadLine(); //阻塞应用进程
}
ContinueWith 本身返回一个 task,它可以用它来附加更多的 Continuation。但是,必须直接处理 AggregateException:
ContinueWith 对于并行编程来说是非常有用的。
创建 Task 除了通过 Task.Run(),还可以使用 TaskCompletionSource 来创建。
TaskCompletionSource 可以在稍后开始和结束的任意操作中创建 Task
TaskCompletionSource 对于 IO-Bound 类工作比较理想
使用TaskCompletionSource初始化一个实例即可。它有一个Task属性可返回一个Task 。该Task完全由TaskCompletionSource 对象控制 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G6rTubgm-1604878145491)(C:\Users\86183\Desktop\学习笔记\图片\image-20201105234308773.png)]
TaskCompletionSource 中有如上图的方法,调用人一个方法都会给 Task 发信号(故障、完成、取消)。这些方法只能调用一次,如果再次调用,则:
public static void Main(string[] args)
{
var tcs = new TaskCompletionSource<int>();
new Thread(() =>
{
Thread.Sleep(5000);
tcs.SetResult(42);
})
{
IsBackground = true;
}.Start();
Task<int> task = tcs.Task;
Console.WriteLine(task.Result);
}
自定义 Task.Run() 方法
public static void Main(string[] args)
{
Task<int> task = Run(() =>
{
Thread.Sleep(5000);
return 42;
});
}
//调用这个方法相当于调用 Task.Factory.StartNew
//并使用 TaskCreationOptions.LongRunning 选项来创建非线程池的线程
static Task<TResult> Run<TResult>(Func<TResult> function)
{
var tcs = new TaskCompletionSource<TResult>();
new Thread(() =>
{
try
{
tcs.SetResult(function()); //运行 function 委托
}
catch(System.Exception ex)
{
tcs.SetException(ex);
}
}).Start();
return tcs.Task;
}
TaskCompletionSource 创建 Task,但并不占用线程。
public static void Main(string[] args)
{
var awaiter = GetAnswerToLife().GetAwaiter(); //生成一个 Continuation
awaiter.OnCompleted(() =>
{
Console.WriteLine(awaiter.GetResult());
});
Console.ReadKey();
}
//调用这个方法相当于调用 Task.Factory.StartNew
//并使用 TaskCreationOptions.LongRunning 选项来创建非线程池的线程
static Task<int> GetAnswerToLife()
{
var tcs = new TaskCompletionSource<int>();
var timer = new System.Timers.Timer(5000) { AutoReset = false };
timer.Elapsed += delegate { timer.Dispose(); tcs.SetResult(42); };
timer.Start();
return tcs.Task;
}
将上述代码封装成更加通用的方法:
public static void Main(string[] args)
{
Delay(5000).GetAwaiter().OnCompleted(() => Console.WriteLine(42));
// 5秒钟之后,Continuation 开始的时候,才占用线程
}
//注意:因为没有非泛型版本的 TaskCompletionSource,此处用object妥协
static Task Delay(int milliseconds)
{
var tcs = new TaskCompletionSource<object>();
var timer = new System.Timers.Timer(milliseconds) { AutoReset = false };
timer.Elapsed += delegate { timer.Dispose(); tcs.SetResult(42); };
timer.Start();
return tcs.Task;
}
.net Core 自带方法,相当于异步版本的 Thread.Sleep 方法
public static void Main(string[] args)
{
Delay(5000).GetAwaiter().OnCompleted(() => Console.WriteLine(42));
// 5秒钟之后,Continuation 开始的时候,才占用线程
Task.Delay(5000).GerAwaiter().OnCompleted(() => Console.WriteLine(42));
Task.Delay(5000).ContinueWith(ant => Console.WriteLine(42));
// Task.Delay 相当于异步版本的 Thread.Sleep
}
//注意:因为没有非泛型版本的 TaskCompletionSource,此处用object妥协
static Task Delay(int milliseconds)
{
var tcs = new TaskCompletionSource<object>();
var timer = new System.Timers.Timer(milliseconds) { AutoReset = false };
timer.Elapsed += delegate { timer.Dispose(); tcs.SetResult(42); };
timer.Start();
return tcs.Task;
}