异步方法和TPL: async / await / Parallel

封装

我们要把上面这个Task封装成方法,怎么办?

最重要的一点,这个方法要能返回生成的random,后面的代码要用!

public static Task getRandom() { return Task.Run(() => { Thread.Sleep(500); //模拟耗时 return new Random().Next(); }); }

@想一想@:应该如何调用这个方法?(_提示:不要直接__getRandom().Result_)

假如我们还需要进一步的封装,添加一个方法Process,里面调用getRandom()并把其结果输出:

public static void Process() { Task task = getRandom(); Console.WriteLine(task.Result); }

故技重施,好像不行了,这次……

@想一想@:再让Process()返回Task行不行?一个Task套另一Task会出现什么情况?

在getRandom()和Process()中展示线程Id看一看:

Console.WriteLine("in getRandom() with Thread-" + Thread.CurrentThread.ManagedThreadId);

在.NET core的I/O类库中,我们会发现这样的方法:

public static Task AppendAllLinesAsync(string path, IEnumerable contents, Encoding encoding, CancellationToken cancellationToken = default); public static Task ReadAllBytesAsync(string path, CancellationToken cancellationToken = default);

注意:

  • 方法名被添加了Async后缀(推荐命名规范)
  • 方法的返回类型为Task或Task

异步方法

.NET为我们提供了简洁优雅的异步方法,只需要两个关键字:

async 和 await

被async标记的方法被称为异步方法,

  • 但是,async不一定(没有强制力保证)异步。同步的方法一样可以标记async。async的作用只是:
  • 告诉编译器方法中可以出现await。如果只有async没有await,报编译时警告

只有await没有async,报编译错误。

static async void Process() { int random = await getRandom(); Console.WriteLine(random); }

await,可以理解为:异步(async)等待,后接 awaitable 实例。

我们可以简单的把awaitable理解成Task。

非阻塞等待

异步方法一直同步运行,直到 await。

从 await 开始异步(分叉):

  • 执行 awatable 中的内容,同时
  • 返回方法的调用处,执行其后内容

直到 awaitable 中内容执行完毕,才暂停方法调用处内容,继续执行await之后的代码。

异步方法执行完毕,继续方法调用处内容。

以上述代码为例:

//33 --> 44 --> 45 --> 46 --> 47 --> 调用异步方法处 // +--> 52 --> 57-- + 被调用异步方法 // +--> 35 --> 38 +--> 39 awaitable

注意:如果52行之前还有普通(非异步)代码,这些代码不会被异步执行。

await不像Wait()或Result一样,

开始(但不是立即或同步的)
async和await会不会开启一个新的任务(或者线程)?不会。

异步方法分为两种:

返回 void 或 Task

public static async void Getup() { Console.WriteLine($"before await-1 with thread {Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"before await-2 with thread {Thread.CurrentThread.ManagedThreadId}"); //await 之前的代码,在主线程上运行 // await Task.Run(()=> { Console.WriteLine($"in await with thread {Thread.CurrentThread.ManagedThreadId}"); }); // Console.WriteLine($"after await-3 with thread {Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"after await-4 with thread {Thread.CurrentThread.ManagedThreadId}"); }

从await开始,代码开始分叉(只是异步,不一定新开线程):

  • 一边执行await后的表达式(Task)
  • 一边返回到方法调用者处继续执行

直到await后的Task执行完毕,才会返回async方法,继续执行其await(非阻塞)之后的剩余代码。

Console.WriteLine($"before async-1 with thread {Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"before async-2 with thread {Thread.CurrentThread.ManagedThreadId}"); Getup(); for (int i = 0; i < 10; i++) { //Getup()里await部分的运行,会打乱这里代码的同步运行 Console.WriteLine($"after async-{3 + i} with thread {Thread.CurrentThread.ManagedThreadId}"); }

实质上,await采用的是Task的ContinueWith()机制:await 之后的方法内代码,被 await Task 执行完毕后调用。

对比演示:

  • 非异步方法:只有Task异步执行
  • 调用Wait()的非异步方法:Wait()会阻塞当前线程进行等待

异步方法中的 void 可以被直接替换成 Task(推荐),以便于该方法进一步的被 await 传递。

void通常做为顶级(top-level)方法使用。

思考:当async方法中抛出异常,void方法和Task方法的区别?

返回Task

返回值被Task包裹,写成Task,T指方法体内声明返回的类型

//方法的声明:返回的是Task public static async Task Getup() { int result = await Task.Run(() => { Thread.Sleep(500); Console.WriteLine($"at await in Getup() with thread {Thread.CurrentThread.ManagedThreadId}"); return new Random().Next(); }); //方法体内,返回的是int return result; }

特别注意:不能直接Getup().Result 或 await Getup()取值,否则……

思考:和直接返回Task的区别?

任务并行库Task Parallel Library

.NET中System.Threading 和System.Threading.Tasks名称空间下的类库

简化异步/并行开发,在底层实现:

  • 动态调整并行规模
  • 处理分区
  • 线程(池)调度(器)等……

于Task的并行

最简单的例子,Parallel.Invoke():

for (int i = 0; i < 5; i++) { Console.WriteLine(); Parallel.Invoke( () => { Console.WriteLine(i + $":task-{Task.CurrentId} in thread-{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"task-{Task.CurrentId} begin in thread-{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"task-{Task.CurrentId} in thread-{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"task-{Task.CurrentId} end in thread-{Thread.CurrentThread.ManagedThreadId}"); }, () => { Console.WriteLine(i + $":task-{Task.CurrentId} in thread-{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"task-{Task.CurrentId} in begin in thread-{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"task-{Task.CurrentId} in thread-{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"task-{Task.CurrentId} in end in thread-{Thread.CurrentThread.ManagedThreadId}"); } ); }

其他方法:

  • For循环

    Parallel.For(0, 10, x => { Console.WriteLine(x); });

  • ForEach

    Parallel.ForEach(Enumerable.Range(1,10), x => Console.WriteLine(x));

引入线程数组:Task[]

  • WaitAll / WaitAny:
  • WhenAll / WhenAny:

对比以下代码,体会 await 的 continuation:

public static async Task Getup() { //await Task.Run(() => { Console.WriteLine("洗脸"); }); //await Task.Run(() => { Console.WriteLine("刷牙"); }); //await Task.Run(() => { Console.WriteLine("吃早餐"); }); //await Task.Run(() => { Console.WriteLine("背单词"); }); Task[] tasks = { Task.Run(() => { Console.WriteLine("洗脸"); }), Task.Run(() => { Console.WriteLine("刷牙"); }), Task.Run(() => { Console.WriteLine("吃早餐"); }), Task.Run(() => { Console.WriteLine("背单词"); }) }; await Task.WhenAll(tasks); }

补充:

Delay()

FromResult()

AsyncState

并行Linq Parallel LINQ (PLINQ)

仅适用于Linq to Object,主要的措施是:对数据源进行分区,然后多核并发运行(保守模式:如果能不并发就不并发)

核心方法:AsParallel(),在数据源后添加。

try { IEnumerable numbers = Enumerable.Range(0, 1000); var filtered = numbers.AsParallel() //.Where(n => n % 11 == 0) .Where(n => 8 % (n > 100 ? n : 0) == 0) ; filtered.ForAll(f => Console.WriteLine(f)); } catch (AggregateException ae) { ae.Handle(e => { Console.WriteLine(e); return true; }); }

ForAll():同样可以并发执行

仍然是AggregateException异常

最佳实践

使用异步/并行的副作用(side effect):

  1. 增加代码的复杂性(尤其是bug调试)
  2. 异步/并行的切换需要消耗额外的资源

简单理解:

  • 锁、死锁(Deadlock)、资源争夺(race condition)
  • 线程安全 (Thread Safty)
  • 天下没有免费的午餐
  • 越是复杂精巧的东西越不“耐操”(健壮性robust)

总是最后考虑异步/并行:(个人建议)

  • 总是在最后考虑异步/并发(尤其是B/S架构)
  • 确定性能瓶颈
  • 确定该瓶颈可以通过异步/并行的方法解决

你可能感兴趣的:(c++)