前些天跟大佬们在群里讨论如何在不使用构造函数,不增加方法参数的情况下把一个上下文注入到方法内部使用,得出的结论是 AsyncLocal 。感叹自己才疏学浅,居然才知道有 AsyncLocal 这种神器。于是赶紧恶补一下。

ThreadLocal

要说 AsyncLocal 还得先从 ThreadLocal 说起。ThreadLocal 封装的变量,可以在线程间进行隔离。不同线程对同一个变量的修改只在当前线程有效。这个应该大家都比较熟悉不多说了。下面简单演示一下:threadLocal 初始值为1,然后启动多个线程对这个变量进行修改,最后主线程等待1秒,保证其它线程都执行成功后再次打印threadLocal的值。

ThreadLocal<int> threadLocal = new ThreadLocal<int>();
threadLocal.Value = 1;
Console.WriteLine("thread id {0} value:{1} START", Thread.CurrentThread.ManagedThreadId, threadLocal.Value);new Thread(() => {threadLocal.Value = 2;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, threadLocal.Value);
}).Start();
new Thread(() => {threadLocal.Value = 3;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, threadLocal.Value);
}).Start();
new Thread(() => {threadLocal.Value = 4;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, threadLocal.Value);
}).Start();
new Thread(() => {threadLocal.Value = 5;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, threadLocal.Value);
}).Start();
new Thread(() => {threadLocal.Value = 6;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, threadLocal.Value);
}).Start();Thread.Sleep(1000);
Console.WriteLine("thread id {0} value:{1} END", Thread.CurrentThread.ManagedThreadId, threadLocal.Value);Console.Read();

输出:

Hello, World!
thread id 1 value:1 START
thread id 7 value:2
thread id 8 value:3
thread id 9 value:4
thread id 10 value:5
thread id 11 value:6
thread id 1 value:1 END

通过一系列线程修改后 threadLocal 的值在 1 号线程始终为 1 ,这也符合我们对 ThreadLocal 预期。

当 ThreadLocal 遇到 await

上面的示例我们使用的是 new Thread 的办法进行多线程操作,现在这种做法已经很少见了。我们现在更多的时候会使用 async/await Task 来帮我们做多线程异步操作。这个时候我们的 ThreadLocal 就会力不从心了,让我们改造一下代码:我们把 new Thread 全部改造成 Task.Run 来执行修改变量的操作。

ThreadLocal<int> threadLocal = new ThreadLocal<int>();
threadLocal.Value = 1;
Console.WriteLine("thread id {0} value:{1} START", Thread.CurrentThread.ManagedThreadId, threadLocal.Value);await Task.Run(() => {threadLocal.Value = 2;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, threadLocal.Value);
});
await Task.Run(() => {threadLocal.Value = 3;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, threadLocal.Value);
});
await Task.Run(() => {threadLocal.Value = 4;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, threadLocal.Value);
});
await Task.Run(() => {threadLocal.Value = 5;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, threadLocal.Value);
});
await Task.Run(() => {threadLocal.Value = 6;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, threadLocal.Value);
});
Console.WriteLine("thread id {0} value:{1} END", Thread.CurrentThread.ManagedThreadId, threadLocal.Value);Console.Read();

输出:

Hello, World!
thread id 1 value:1 START
thread id 7 value:2
thread id 8 value:3
thread id 10 value:4
thread id 11 value:5
thread id 12 value:6
thread id 11 value:5 END

通过输出我们可以看到 START 跟 END 的输出已经不一样了。至于为什么,如果理解 Task 的原理,其实也很好理解。简单来说,Task 的异步是一种基于状态机实现方式,编译器碰到 await 会把代码编译成一个代码块,表示一种状态。Task 的任务调度器会调度空闲线程去处理每一个状态。当一个状态完成后,调度器调度一个空闲线程去处理下一个任务,这样一个接一个处理。这里最大的困扰其实是主观上的当前线程(打印 START 跟 END 的线程)已经不是同一个了,打印 START 的是 1 号线程,打印 END 的是 11 号线程,那么 ThreadLocal 自然不适合这种场景了。

AsyncLocal

上面我们已经知道 ThreadLocal 已经不适合在新的 TPL 模型下的多线程变量隔离。那么我们该如何进行应对呢?答案就是 AsyncLocal 。
让我们改造下代码,把 Threadlocal 替换成 AsyncLocal ,其它不变,运行一下代码。

AsyncLocal<int> asyncLocal = new AsyncLocal<int>();
asyncLocal.Value = 1;
Console.WriteLine("thread id {0} value:{1} START", Thread.CurrentThread.ManagedThreadId, asyncLocal.Value);await Task.Run(() => {asyncLocal.Value = 2;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, asyncLocal.Value);
});
await Task.Run(() => {asyncLocal.Value = 3;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, asyncLocal.Value);
});
await Task.Run(() => {asyncLocal.Value = 4;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, asyncLocal.Value);
});
await Task.Run(() => {asyncLocal.Value = 5;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, asyncLocal.Value);
});
await Task.Run(() => {asyncLocal.Value = 6;Console.WriteLine("thread id {0} value:{1}", Thread.CurrentThread.ManagedThreadId, asyncLocal.Value);
});
Console.WriteLine("thread id {0} value:{1} END", Thread.CurrentThread.ManagedThreadId, asyncLocal.Value);Console.Read();

输出:

thread id 1 value:1 START
thread id 6 value:2
thread id 7 value:3
thread id 8 value:4
thread id 11 value:5
thread id 7 value:6
thread id 7 value:1 END

结果如我们所愿, START 跟 END 的值是一致的。我们可以看到虽然线程发生了切换,但是值被很好的保留在了当前流程下。
让我们使用另外一个代码实例来演示下 AsyncLocal 的特性。上面的代码演示的是一个 Task 接一个 Task 的场景,一下我们演示下 Task 嵌套 Task 的场景。

AsyncLocal<int> asyncLocal = new AsyncLocal<int>();
//block 1
asyncLocal.Value = 1;
Console.WriteLine("thread id {0} value:{1} START", Thread.CurrentThread.ManagedThreadId, asyncLocal.Value);await Task.Run(async () =>{//block 2asyncLocal.Value = 2;Console.WriteLine("thread id {0} value:{1} ", Thread.CurrentThread.ManagedThreadId, asyncLocal.Value);await Task.Run(() => {//block 3asyncLocal.Value = 3;Console.WriteLine("thread id {0} value:{1} ", Thread.CurrentThread.ManagedThreadId, asyncLocal.Value);});Console.WriteLine("thread id {0} value:{1} ", Thread.CurrentThread.ManagedThreadId, asyncLocal.Value);});Console.WriteLine("thread id {0} value:{1} END", Thread.CurrentThread.ManagedThreadId, asyncLocal.Value);Console.Read();

输出

thread id 1 value:1 START
thread id 6 value:2
thread id 7 value:3
thread id 7 value:2
thread id 7 value:1 END

跟你预期的结果一致吗?结果为:1 2 3 2 1 。AsyncLocal 的变量值会被隔离在每个 Task 流程内,就算嵌套,子流程对变量的修改也不会影响到父流程的值。

AsyncLocal 实用

AsyncLocal 的特性说的差不多了。那么 AsyncLocal 到底该使用在什么场景呢?
当我们重构代码的时候如果需要把一个上下文参数传递进去,最傻瓜的办法就是在所有的调用类的构造函数上加入这个参数,或者在所有的方法调用上加入这个参数。但是这种办法是破坏性比较大的,因为函数签名被破坏意味着接口(广义上)约束被破坏了。这个时候我们可以通过 AsyncLocal 把上下文传递进去。
定义一个 MyContext 类:

public class MyContext : IDisposable{static AsyncLocal<MyContext> _scope = new AsyncLocal<MyContext>();public MyContext(object val){Value = val;_scope.Value = this;}public object Value { get;}public static MyContext? Current{get{return _scope.Value;}}public void Dispose(){if (Value != null){(Value as IDisposable)?.Dispose();}}}

假设我们已经有了 Func1 方法,现在在不破坏任何接口约束的情况下可以把 MyContext 直接通过静态变量 MyContext.Current 获取到。

void Func1()
{Console.WriteLine(MyContext.Current?.Value);
}using (var ctx = new MyContext("context 1"))
{Func1();
}using (var ctx = new MyContext("context 2"))
{Func1();
}
using (var ctx = new MyContext("context 3"))
{await Task.Run(Func1);await Task.Run(Func1);await Task.Run(Func1);
}

另外一个实现其实是大家非常常见的 HttpContextAccessor 。ASP.NET Core 下我们获取 HttpContext 会通过 HttpContextAccessor 获取。HttpContextAccessor 通常被注册为单例。大家有没有想过为啥单例的 HttpContextAccessor.HttpContext 变量不会被多线程或者异步方法打乱?原因也就在于 AsyncLocal 。源码在这 HttpContextAccessor ,并不复杂大家可以看看。

关注我的公众号一起玩转技术

从 ThreadLocal 到 AsyncLocal相关推荐

  1. 浅谈AsyncLocal,我们应该知道的那些事儿

    [导读]最近查看有关框架源码,发现AsyncLocal这玩意水还挺深,于是花了一点功夫去研究,同时对比ThreadLocal说明二者区别以及在何时场景下使用AsyncLocal或ThreadLocal ...

  2. FastThreadLocal吞吐量居然是ThreadLocal的3倍

    目前关于FastThreadLocal的很多文章都有点老有点过时了(本文将澄清几个误区),很多文章关于FastThreadLocal介绍的也不全,希望本篇文章可以带你彻底理解FastThreadLoc ...

  3. 正确理解ThreadLocal

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt107 首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的, ...

  4. 什么是ThreadLocal

    顾名思义它是local variable(线程局部变量).它的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突.从线 ...

  5. 为什么jdk源码推荐ThreadLocal使用static

    ThreadLocal是线程私有变量,本身是解决多线程环境线程安全,可以说单线程实际上没必要使用. 既然多线程环境本身不使用static,那么又怎么会线程不安全.所以这个问题本身并不是问题,只是有人没 ...

  6. Spring源码分析【6】-ThreadLocal的使用和源码分析

    Spring代码使用到了ThreadLocal java.lang.ThreadLocal.set getMap java.lang.Thread.threadLocals定义 回到set 如果map ...

  7. threadlocal使用场景_深入剖析ThreadLocal

    点击上方 IT牧场 ,选择 置顶或者星标 技术干货每日送达 朋友们在遇到线程安全问题的时候,大多数情况下可能会使用synchronized关键字,每次只允许一个线程进入锁定的方法或代码块,这样就可以保 ...

  8. ThreadLocal源码分析

    ThreadLocal的作用 Java对象是线程间共享的,但有时我们需要一些线程间隔离的对象,该对象只能由同一个线程读写,对其他线程不可见.ThreadLocal正式提供了这样的机制,详细使用方式请参 ...

  9. ThreadLocal的使用场景

    最近项目中遇到如下的场景:在执行数据迁移时,需要按照用户粒度加锁,因此考虑使用排他锁,迁移工具和业务服务属于两个服务,因此需要使用分布式锁. 我们使用缓存(Tair或者Redis)实现分布式锁,具体代 ...

最新文章

  1. AndroidManifest.xml配置文件详解
  2. 我有文章了,但也不想搞学术了
  3. PowerShell(PHPStorm terminal with PowerShell)运行git log中文乱码
  4. win7和Ubuntu16.04之间相互远程控制
  5. span组件内容的刷新(笔记)
  6. while 中 break 与 continue 的区别
  7. less,sass,stylus配置和应用教程及三者比较
  8. 在 uniapp 中使用 阿里巴巴矢量图标
  9. 通讯录_通讯录拦截防爆
  10. android 息屏锁频录音、录视频、电话自动录音
  11. 如何快速计算出三相电流的数值呢
  12. greasemonkey油猴子初学过程中遇到的问题
  13. 微信小程序如何保存图片到本地?
  14. SpringBoot的幕后推手,java互联网架构师系统vip课程
  15. 如何删除PPT密码?附详细方法
  16. web前端开发主要课程,jquery源码实现原理
  17. OpenWRT使用SNMP监测网络状态
  18. 计算机桌面设备管理器,电脑设备管理器在哪里打开【图文】
  19. 10、第二章物理层习题及参考答案
  20. C语言:L1-061 新胖子公式 (10 分)

热门文章

  1. mysql表空间被占用,同名表无法创建或导入
  2. oracle io lost,磁盘IO故障
  3. git安装与配置_git 安装及基本配置
  4. 洛谷P2587 [ZJOI2008] 泡泡堂
  5. 一款不错的编程字体Source Code Pro
  6. ArcEngine 没有Esri.ArcGis.GeoAnalyst 命名空间
  7. Managing Gigabytes--文本压缩
  8. [导入]心平气和,千佳骈集;意粗性躁,一事无成
  9. React - 状态提升
  10. 8支团队正在努力构建下一代Ethereum