C# 多线程编程 0x01
概述
多线程主要有异步、同步、并行等模式。
在.Net中,共有过三种比较主要的异步编程方式:
- 异步编程模型(APM - Asynchronous Programming Model)
- 基于事件的异步模式(EAP - Event-based Asynchronous Pattern)
- 基于任务的异步模式(TAP - Task-based Asynchronous Pattern)
其中,TAP自.Net 4被提出后,就作为最主要也是微软推荐的异步实现方法,因此注重深入该模式。
另外,对于并行计算的需求,也增加了一个 任务并行库(TPL - Task Parallel Library) 来使的大量并行任务的创建更加简单。
如果对于其他异步编程模式有兴趣深入了解可以点击查看微软关于此的文档:异步编程模式
异步
基于任务的异步模式(TAP)
TAP与其他异步模式最大的不同在于,其用一个方法来表示异步操作的全过程,其名也因此得来。
为此C#加入了async和await关键字为TAP提供语言级别的支持。
TAP是基于System.Threading.Tasks.Task及其衍生System.Threading.Tasks.Task<TResult>两个类实现的,它们都被用于表示任意的异步任务,其中后者用于有返回值的情况。
简单实践
Task类最主要的功能就是可以配合async及await关键字,十分简单的实现异步操作。
而其中async用于修饰方法,仅有async修饰的方法中才可以使用await关键字,async仅能修饰返回值为Task/void/IAsyncOperation的方法。因此async无法修饰程序入口点。
下面是一个3秒后返回一个字符串的方法:
static string Greeting(string name)
{
Task.Delay(3000).Wait();
return $"Hello, {name} \tCurrent Thread: {Thread.CurrentThread.ManagedThreadId}";
}
如果简单的顺序调用,那么每个都会占用主线程3秒时间。但通过Task类可以很简单的将其放入其他线程中运行:
static Task<string> GreetingAsync(string name)
{
return Task.Run<string>(()=> { return Greeting(name); } );
}
为了在等待其结果并打印的时候不阻塞主线程,这时候就需要使用await关键字,注意,只有经过async修饰的方法内才可以使用await关键字:
private async static void CallerWithAsync()
{
string res = await GreetingAsync("Johnson"); //await关键字用于等待任务结果并取回结果,但不会阻塞主线程,因为该方法是异步的,其等待并不在主线程。
Console.WriteLine(res);
}
这样一来,只需要直接调用CallerWithAsync方法就可以实现异步调用:
static void Main(string[] args)
{
CallerWithAsync();
CallerWithAsync();
Console.WriteLine("Hello, Newguy");
Console.ReadLine(); //如果没有这一行,那么主线程就会因为没有阻塞而直接结束,那就看不到结果了。
}
输出如下,其中后两行在3秒后同时出现,可以看到它们所在的线程均不同:
Hello, Newguy
Hello, Johnson Current Thread: 3
Hello, Johnson Current Thread: 4
如果是多个返回类似的异步方法可以组合一并返回,这样效率更高。
private async static void CallerWithAsyncWhenAll()
{
Task<string> s1 = GreetingAsync("Bob");
Task<string> s2 = GreetingAsync("Holy"); //定义时并不会执行
//另外还有一个WhenAny方法,该方法和All不一样的是当组合中任一结果返回则会返回。
string[] res = await Task.WhenAll(s1, s2);
Console.WriteLine($"All Done. s1 = {res[0]}, s2 = {res[1]}");
}
异常处理
异步任务也可以应用异常,但需要万分注意捕获异常的块和异步方法的生存期。
简单的看一下,下面代码会在指定延迟后抛出异常。
static async Task ThrowAfter(int ms, string message)
{
await Task.Delay(ms);
throw new Exception(message);
}
如果我们用普通的方式去捕获,就无法捕捉到异常。
static async void HandelAwaitError()
{
try
{
ThrowAfter(2000, "no await");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
因为该异常块会先于ThrowAfter结束,也就是试图捕捉异常的代码必须等待可能出现异常的方法返回。因此需要await ThrowAfter();的形式调用。
对于一个块要捕捉多个异常,则需要一点取巧,首先是要利用Task.WhenAll方法确保多个异步方法都执行完毕。其次因为一般的catch块只能接收一个异常参数,因此需要在外部定义一个catch块能访问到的Task变量,并以该变量接受Task.WhenAll方法的结果,WhenAll方法会将收到的所有异常封装在一起返回。如下:
static async void HandelMultipleAwaitError()
{
Task taskRes = null;
try
{
Task t1 = ThrowAfter(2000, "Mutiple exception in 2s.");
Task t2 = ThrowAfter(1000, "Mutiple exception in 1s.");
await (taskRes = Task.WhenAll(t1, t2));
}
catch(Exception exception)
{
//不使用ex而是使用可以访问到的taskRes
foreach(var ex in taskRes.Exception.InnerExceptions)
Console.WriteLine(ex.Message)
}
}
任务取消
有时候异步方法并不总是要运行结束,有时候我们需要提前结束。
正常的做法是利用可取消信令实现,CancellationTokenSource类提供该信令的实现,该信令并非提供强制结束的能力,而是为告知异步方法需要中止提供一个简单的方法,也就是可以简单理解为传信员,因此要注意该信令的可见性。下面有个简单例子:
private static CancellationTokenSource cts; //在外层定义信令或通过传递的方式让前台线程可见该信令,否则就无法发送中止信号了。
static void CancelableTask()
{
cts = new CancellationTokenSource();
Task.Run(async () => {
while (true) {
//在异步方法中可以通过IsCancellationRequsted属性来检查是否请求中止,也可以用下面的方法在请求中止时直接中止并抛出异常。
//由此可见CancellationToken并不是强制性的,而是提供了一种交互式的接口,这样一来异步任务可以在发现请求中止时合理的做后处理工作。
cts.Token.ThrowIfCancellationRequested();
Console.WriteLine("Program Runing...");
await Task.Delay(1000);
}
}, cts.Token);
}
static void Main(string[] args)
{
Console.ReadLine();
cts?.Cancel(); //注意使用空值传播运算符,防止取消时信令仍未定义。这是一种良好的习惯。
}
并行
C#的库提供了Parallel类方便并行编程,他们的简单介绍在另一篇文章中:C# 并行编程概述
532

被折叠的 条评论
为什么被折叠?



