【.NET深呼吸】基于异步上下文的本地变量(AsyncLocal)

在开始吹牛之前,老周说两个故事。

第一个故事是关于最近某些别有用心的人攻击.net的事,其实我们不用管它们,只要咱们知道自己是.net爱好者就行了,咱们就是因为热爱.net才会选择它。这些人在这段时间攻击.net,估计和.net的开源、跨平台有关,并且,据说VS 2015 Update 1会进一步深化和扩展全平台,估计有些人是沉不住气了,毕竟他们用的开发工具是比VS落后了四个多世纪的。最近又出了个Visual Studio Dev Essentials计划。

所以嘛,对于这些人,我把林妹妹的一首诗送给他们:

无端弄笔是何人?

作践.net太轻狂。

不悔自家无见识,

却将丑语怪他人。

接下来说说第二个故事。或许不少应届毕业生都在准备或者已经在找实习单位,或找工作了,于是有朋友私信老周,希望老周说说简历如何做的事情。这个嘛,一来,老周不是简历专家;二来,在本文中不好展开去谈,过一两天吧,老周找时间再写篇烂文,专门说说这个事;三来,仅为一家之言,以供参考。

============================================

好了,与主题无关的话说完了,下面开始说正事。记得在X月前,老周写过有关ThreadLocal的文章,也就是基于线程的本地变量存储。使用这个ThreadLocal的前提是:

1、变量必须是多个线程共享的,如果是线程范围内的局部变量就不需要了。

2、希望每个线程都能读写独立的变量值。

今天,老周再介绍一个功能和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的功能了吧。

本文示例下载地址

时间: 2024-10-14 05:24:28

【.NET深呼吸】基于异步上下文的本地变量(AsyncLocal)的相关文章

SQL Server-聚焦事务对本地变量、临时表、表变量影响以及日志文件存满时如何收缩(三十一)

前言 接下来我们将SQL Server基础系列还剩下最后几节内容结束,后续再来讲解SQL Server性能调优,我们开始进入主题. SQL Server事务对本地变量影响 事务对变量影响具体是指什么意思呢,换句话说就是当我们回滚事务和提交事务之后对本地变量是否起作用呢,下面我们来看下具体例子. PRINT '回滚事务之后测试' DECLARE @FlagINT INT SET @FlagInt = 1 PRINT @FlagInt ---- 此时变量值为1 BEGIN TRANSACTION S

线程本地变量ThreadLocal

一.本地线程变量使用场景 并发应用的一个关键地方就是共享数据.如果你创建一个类对象,实现Runnable接口,然后多个Thread对象使用同样的Runnable对象,全部的线程都共享同样的属性.这意味着,如果你在一个线程里改变一个属性,全部的线程都会受到这个改变的影响. 有时,你希望程序里的各个线程的属性不会被共享. Java 并发 API提供了一个很清楚的机制叫本地线程变量即ThreadLocal. 模拟ThreadLocal类实现:线程范围内的共享变量,每个线程只能访问他自己的,不能访问别的

declare的用法:定义环境变量;把环境变量修改为本地变量;是变量变为只读(和readonly一样)。

declare的法;  定义环境变量:declare -x 变量名 修改环境变量为本地变量: declare +x 环境变量名 使变量变为可读:declare -r 变量名 [[email protected] opt]# bb=10 [[email protected] opt]# declare -r bb //把变量变为可读(不能修改和删除) [[email protected] opt]# bb=20 bash: bb: readonly variable [[email protect

.Net学习难点讨论系列17 - 线程本地变量的使用

*:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* BLOCKS =============================================================================*/ p, blockquote, ul, ol, dl, table, pre { margin: 15px 0; } /* HEAD

linux基本变量学习:本地变量、环境变量、特殊变量、位置变量

shell: 弱类型编程语言 强:变量在使用前,必须事先声明,甚至还需要初始化: 弱:变量用时声明,甚至不区分类型: 变量赋值:VAR_NAME=VALUE bash变量类型: 环境变量 本地变量(局部变量) 位置变量 特殊变量 本地变量: set VARNAME=VALUE: 作用域为整个bash进程: 局部变量: local VARNAME=VALUE:作用域为当前代码段: 环境变量:作用域为当前shell进程及其子进程: export VARNAME=VALUE VARNAME=VALUE

一个整数,大于0,不用循环和本地变量,按照 n, 2n, 4n, 8n 的顺序递增,当值大于5000 时,把值按照指定顺序输出来。

1 package sfbc; 2 /** 3 * 一个整数,大于0,不用循环和本地变量,按照 n, 2n, 4n, 8n 的顺序递增,当值大于5000 4 时,把值按照指定顺序输出来. 5 例: n=1237 6 则输出为: 7 1237, 8 2474, 9 4948, 10 9896, 11 9896, 12 4948, 13 2474, 14 1237, 15 提示:写程序时,先致谢按递增方式的代码,写好递增的以后,再增加考虑递减部分. 16 * @author trfizeng 17

线程本地变量ThreadLocal源码解读

  一.ThreadLocal基础知识   原始线程现状: 按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步.但是Spring中的各种模板类并未采用线程同步机制,因为线程同步会影响并发性和系统性能,而且实现难度也不小. ThreadLocal在Spring中发挥着重要的作用.在管理request作用域的bean,事务管理,任务调度,AOP等模块中都出现了它的身影. ThreadLocal介绍: 它不是一个线程,而是线程的一个本地化

linux环境变量与本地变量

两者不同的是. 环境变量可以在shell的子进程中使用, 而本地变量不同. 每当连接上服务器时,服务器就会通过帐号密码运行一个SHELL,我们所做的工作都在这个SHELL上,特殊方法除外(如,守护进程) 我们在SHELL所敲击的命令,实质上都是shell的子进程.环境变量就可以在子进程中访问.而本地变量只能在SHELL本身访问.

本地变量和环境变量

nux的变量类型有两种.本地变量和环境变量(全局变量) 你可以使用的linux命令有,export和declare用法:export 变量名=值 例如:export total=50declare -x 变量名=值 例如:declare -x total=19 使用set命令可以查看你系统当前的所有的变量使用env命令可以查看你系统当前的所有环境变量