在开始吹牛以前,老周说两个故事。异步
第一个故事是关于最近某些别有用心的人攻击.net的事,其实咱们不用管它们,只要我们知道本身是.net爱好者就好了,我们就是由于热爱.net才会选择它。这些人在这段时间攻击.net,估计和.net的开源、跨平台有关,而且,听说VS 2015 Update 1会进一步深化和扩展全平台,估计有些人是沉不住气了,毕竟他们用的开发工具是比VS落后了四个多世纪的。最近又出了个Visual Studio Dev Essentials计划。async
因此嘛,对于这些人,我把林妹妹的一首诗送给他们:工具
无故弄笔是何人?开发工具
做践.net过轻狂。spa
不悔自家无见识,.net
却将丑语怪他人。线程
接下来讲说第二个故事。或许很多应届毕业生都在准备或者已经在找实习单位,或找工做了,因而有朋友私信老周,但愿老周说说简历如何作的事情。这个嘛,一来,老周不是简历专家;二来,在本文中很差展开去谈,过一两天吧,老周找时间再写篇烂文,专门说说这个事;三来,仅为一家之言,以供参考。code
============================================blog
好了,与主题无关的话说完了,下面开始说正事。记得在X月前,老周写过有关ThreadLocal的文章,也就是基于线程的本地变量存储。使用这个ThreadLocal的前提是:ip
一、变量必须是多个线程共享的,若是是线程范围内的局部变量就不须要了。
二、但愿每一个线程都能读写独立的变量值。
今天,老周再介绍一个功能和ThreadLocal相似的东东——AsyncLocal。
这个主要是用于保存异步等待上下文中的共享变量的值。从C# 5开始,引入了至关简便的异步等待语法,即await关键字调用异步方法,容许异步等待。
即代码在使用await关键字调用异步方法后,当前程序会等待异步方法返回后才会继续执行,但在这个等待过程当中,不会阻塞当前线程,这比起编写委托来回调方便多了。
异步方法是基于Task的自动线程调度,在异步上下文的切换过程当中,有可能会致使数据丢失。好比,在await调用前,对某个变量赋了值,而这个变量是多个线程共享的;当await调用返回后,有可能当前代码仍然处于先前的线程上,但也有可能被调度到其余线程上。这种状况通常发生在与应用程序UI线程无关的代码上,若是异步操做是由UI启动的,一般状况下不会调动异步上下文的线程,然而,若是异步操做是非UI触发的,典型的如在Main入口处启动的,这就颇有可能出现异步上下文处于不一样的线程上的情形。
这样描述太抽象,很难懂,没事,给你们看一个例子就知道了。
先定义一个异步方法:
static async Task RunAsync() { // 输出当前线程的ID Console.WriteLine($"异步等待前,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); // 开始执行异步方法,并等待完成 await Task.Delay(50); // 异步等待完成后,再次输出当前线程的ID Console.WriteLine($"异步等待后,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }
异步方法中,调用了Task.Delay方法,这个方法也是能够异步等待的,所以用await关键字来等待50毫秒。
而后,在Main入口处调用以上异步方法。
static void Main(string[] args) { // 声明一个委托实例 Action act = async () => { await RunAsync(); }; // 执行委托 act(); Console.Read(); }
我这里是先声明一个Action委托实例,并经过Lambda表达式调用异步方法,而且异步等待其完成。由于使用了await关键字的方法上必须标注async修饰符,以说明该方法中出现异步等待代码,可是,Main入口方法上是不容许添加async修饰符的,因此,我就用一个委托来调用。
运行这个例子,你会有惊奇发现,请看,有图有真相。
看到没,await等待前,当前的线程是8,异步等待回来后,当前线程就被自动调度到10上了。
== 在线程8上 await Task.Delay(50); == 在线程10上
从代码上看,await先后是连续的,但实际上,在执行阶段,它们已经处于不一样的线程上了。
那么,我就想啊,若是在此种状况下使用ThreadLocal变量会发生什么事情。试试看。
// 线程共享变量 static ThreadLocal<int> local = new ThreadLocal<int>(); static void Main(string[] args) { // 声明一个委托实例 Action act = async () => { await RunAsync(); }; // 执行委托 act(); Console.Read(); } static async Task RunAsync() { // 给共享变量赋值 local.Value = 53000; // 输出变量的值 Console.WriteLine($"异步等待前:{nameof(local)} = {local.Value}"); await Task.Delay(50); //异步等待 // 异步等待回来,再次输出变量的值 Console.WriteLine($"异步等待后:{nameof(local)} = {local.Value}"); }
上面例子使用了ThreadLocal声明线程间共享变量,在异步方法中,先给这个变量赋值为53000,而后await开始等待,等待返回后,再次输出变量的值。
好,注意看,意外发生了。
哟,有朋友估计会尖叫了,这是咋回事?await前不是给共享的变量赋了值吗,为何等待返回后值会变回默认值0呢。前面老周说了,等待前,等待后是有可能处于不一样的线程上,而ThreadLocal是为每一个线程保存独立的值的。
假设,设置local值为53000是在线程A上执行的,那么,local变量为线程A保留了值53000;当代码执行到await关键字一行后,开始异步等待,而等待返回后,当前代码可能被调度到线程B上了。而53000是为线程A所存储的值,对于线程B,未赋值,因此就获得默认的值0。
很显然,ThreadLocal是不适合在异步上下文中使用的。下面就请出今天的主角——AsyncLocal。
把上面的代码中的ThreadLocal改成AsyncLocal。
// 线程共享变量 static AsyncLocal<int> local = new AsyncLocal<int>(); ……
而后,再运程序,看图。
看到了吧,这下子好了,53000在异步上下文中被保留了。
如今,你明白了AsyncLocal的功能了吧。