.NET | 多线程下的调用上下文 : CallContext
【.NET】| 总结/Edison Zhou
最近在分析现在团队的项目代码(基于.NET Framework 4.5),经常发现一个CallContext的调用,记得多年前的时候用到了它,但是印象已经不深刻了,于是现在来复习一下。
1CallContext是个啥?
如果说,一个对象保证全局唯一,大家肯定会想到一个经典的设计模式:单例模式。但是,如果要使用的对象必须是线程内唯一的呢?
在.NET Framework中,Microsoft给我们设计了一个CallContext类。
命名空间:System.Runtime.Remoting.Messaging
类型完全限定名称:System.Runtime.Remoting.Messaging.CallContext
CallContext类似于方法调用的线程本地存储区的专用集合对象,并提供对每个逻辑执行线程都唯一的数据槽。数据槽不在其他逻辑线程上的调用上下文之间共享。当 CallContext 沿执行代码路径往返传播并且由该路径中的各个对象检查时,可将对象添加到其中。
简而言之,CallContext提供线程(多线程/单线程)代码执行路径中数据传递的能力。
方法 |
描述 |
线程安全 |
SetData |
存储给定的对象并将其与指定名称关联。 |
否 |
GetData |
从System.Runtime.Remoting.Messaging.CallContext中检索具有指定名称的对象 |
否 |
LogicalSetData |
将给定的对象存储在逻辑调用上下文,并将其与指定名称关联。 |
是 |
LogicalGetData |
从逻辑调用上下文中检索具有指定名称的对象。 |
是 |
FreeNamedDataSlot |
清空具有指定名称的数据槽。 |
是 |
HostContext |
获取或设置与当前线程相关联的主机上下文。在Web环境下等于System.Web.HttpContext.Current |
2探究CallContext方法
上面介绍了CallContext提供的核心方法,下面我们就来通过实践来理解一下。
准备工作
这里准备一个User类作为数据传递对象:
public class User
{public string Id { get; set; }public string Name { get; set; }
}
测试1:GetData、SetData 与 FreeNamedDataSlot
测试代码很简单,就是在主线程 和 子线程之中分别传递User对象实例,看看最后的效果。
public void TestGetSetData()
{// 主线程执行Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}");var user = new User(){Id = DateTime.Now.ToString(),Name = "Edison"};CallContext.SetData("key", user);var value1 = CallContext.GetData("key");Console.WriteLine(user == value1);// 异步线程执行Task.Run(() =>{Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}");var value2 = CallContext.GetData("key");Console.WriteLine(value2 == null ?"NULL" : (value2 == value1).ToString());});// 主线程执行Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}");value1 = CallContext.GetData("key");Console.WriteLine(value1 == user);// 清理数据槽CallContext.FreeNamedDataSlot("key");var value3 = CallContext.GetData("key");Console.WriteLine(value3 == null ?"NULL" : (value3 == value1).ToString());
}
上面示例代码的运行结果如下图所示:
根据上图所示的结果,基本可以得出以下两个结论:
1、GetData、SetData方法只能用于单线程环境,如果发生了线程切换,存储的数据也会随之丢失。
2、GetData 和 SetData 可以用于同一线程中的不同地方,传递数据。
可以知道,要在多线程环境下使用,我们需要用到另外两个方法:LogicalSetData 与 LogicalGetData。
测试2:LogicalGetData、LogicalSetData 与 FreeNamedDataSlot
public void TestLogicalGetSetData()
{// 主线程执行Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}");var user = new User(){Id = DateTime.Now.ToString(),Name = "Edison"};CallContext.LogicalSetData("key", user);var value1 = CallContext.LogicalGetData("key");Console.WriteLine(user == value1);// 异步线程执行Task.Run(() =>{Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}");var value2 = CallContext.LogicalGetData("key");Console.WriteLine(value2 == null ?"NULL" : (value2 == value1).ToString());Thread.Sleep(1000);value2 = CallContext.LogicalGetData("key");Console.WriteLine(value2 == null ?"NULL" : (value2 == value1).ToString());});// 主线程执行Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}");// 清理数据槽CallContext.FreeNamedDataSlot("key");var value3 = CallContext.LogicalGetData("key");Console.WriteLine(value3 == null ?"NULL" : (value3 == value1).ToString());
}
这段示例代码的运行结果如下图所示:
根据上图所示的结果,基本可以得出以下三个结论:
1、FreeNamedDataSlot只能清除当前线程的数据槽,不能清除子线程的数据槽;
2、LogicalSetData、LogicalGetData可用于在多线程环境下传递数据;
3、FreeNamedDataSlot清除当前线程的数据槽后,之前已经运行的子任务,不受影响;
测试3:LogicalGetData后修改传递的数据
在多线程环境下传递共享对象数据,如果某个线程通过LogicalGetData后对其进行了修改又重新LogicalSetData会怎样?
public void TestLogicalGetSetDataV2()
{// 主线程执行Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}");var user = new User(){Id = DateTime.Now.ToString(),Name = "Edison"};CallContext.LogicalSetData("key", user);var value1 = CallContext.LogicalGetData("key");Console.WriteLine(user == value1);// 异步线程同步执行:加了.Wait()Task.Run(() =>{Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}");var value2 = CallContext.LogicalGetData("key");Console.WriteLine(value2 == null ?"NULL" : (value2 == value1).ToString());CallContext.FreeNamedDataSlot("key");value2 = CallContext.LogicalGetData("key");Console.WriteLine(value2 == null ?"NULL" : (value2 == value1).ToString());}).Wait();// 异步线程同步执行:加了.Wait()Task.Run(() =>{Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}");var value2 = CallContext.LogicalGetData("key") as User;Console.WriteLine(value2 == null ?"NULL" : (value2 == value1).ToString());value2.Name = "Leo";CallContext.LogicalSetData("key", new User() { Id = DateTime.Now.ToString(), Name = "Jack" }); // 只影响当前线程value2 = CallContext.LogicalGetData("key") as User;Console.WriteLine(value2 == null ?"NULL" : (value2 == value1).ToString());Console.WriteLine($"User.Name={value2.Name}");}).Wait();// 主线程执行Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}");var value3 = CallContext.LogicalGetData("key") as User;Console.WriteLine(value3 == null ?"NULL" : (value3 == value1).ToString());Console.WriteLine($"User.Name={value3.Name}");
}
上面示例代码的运行结果如下图所示:
根据上面的示例运行结果,我们又可以得到以下一些结论:
1、FreeNamedDataSlot只能清除当前线程的数据槽
2、LogicalSetData只会存储当前线程以及子线程的数据槽;
3、LogicalGetData获取的是当前线程或父线程的数据槽对象,拿到的是对象的引用,因此如果对其进行修改,会影响父线程读取的一致性,在关系型数据库中也被称为不可重复读。
4、子线程中使用LogicalSetData改变数据槽的值,不会影响父线程的数据槽,即使他们的key是同一个;
3.NET Core下没有CallContext
在.NET Core下没有CallContext类,取而代之的是使用AsyncLocal代替,实现的是CallContext.LogicalGetData 和 CallContext.SetLogicalCallContext。
例如,下面是一个示例代码,我们可以借助AsyncLocal来自己实现一个CallContext类。如果你是将.NET Framework升级为.NET Core,那么你可能需要自己实现一个CallContext类来代替之前的CallContext:
public static class CallContext
{static ConcurrentDictionary<string, AsyncLocal<object>> state = new ConcurrentDictionary<string, AsyncLocal<object>>();public static void SetData(string name, object data) =>state.GetOrAdd(name, _ => new AsyncLocal<object>()).Value = data;public static object GetData(string name) =>state.TryGetValue(name, out AsyncLocal<object> data) ? data.Value : null;
}
4EF DbContext场景
对于像UnitOfWork这种操作模式,是比较适合于CallContext发挥的地方,让EF DbContext在线程上下文内保持唯一。
注意:这里提到的EF均指EF 而非 EF Core。
因此,我们经常可以看到如下所示的示例代码:
public class DbContextFactory
{public static DbContext CreateDbContext(){DbContext dbContext = (DbContext)CallContext.GetData("dbContext");if (dbContext == null){dbContext = new WebAppEntities();CallContext.SetData("dbContext", dbContext);}return dbContext;}
}
此用法像极了 Cache(缓存)的使用。
But,鉴于目前广泛使用线程池的前提,线程在处理完一个请求之后,并没有被销毁,存储在CallContext中的上下文对象也一直存在,如果是下一次拿出这个线程去处理另一个请求,这个上下文对象其实也在不断的膨胀,只不过比全局的膨胀的稍微慢一些。而且,有时候一个线程并不一定是拿去处理请求了,如果是服务器拿去处理其他的业务,那就可能引发一些其他的问题。
这时,或许我们可以考虑另一个方案,在ASP.NET中的HttpContext中有一个Items属性,它也可以用来保存key-value,这就完美了,一次请求正好对应着一个HttpContext,请求结束,它自动释放,EF上下文也就不存在了。
因此,这里把上面代码中的CallContext改为HttpContext.Current.Items:
public class DbContextFactory
{public static DbContext CreateDbContext(){DbContext dbContext = HttpContext.Current.Items["dbContext"] as DbContext;if (dbContext == null){dbContext = new WebAppEntities();HttpContext.Current.Items["dbContext"] = dbContext;}return dbContext;}
}
其实,HttpContext这个类和CallContext是有关联的,查看源码我们可以发现:HttpContext.Current是通过CallContext.HostContext实现的。
internal static Object Current {get {return CallContext.HostContext;}[SecurityPermission(SecurityAction.Demand, Unrestricted = true)]set {CallContext.HostContext = value;}
}
关于HttpContext.Current:ASP.NET会为每个请求分配一个线程,这个线程会执行我们的代码来生成响应结果, 即使我们的代码散落在不同的地方(类库),线程仍然会执行它们。所以,我们可以在任何地方访问HttpContext.Current获取到与当前请求相关的HttpContext对象,毕竟这些代码是由同一个线程来执行的嘛,所以得到的HttpContext引用也就是那个与请求相关的对象。因此,将HttpContext.Current设计成与当前线程相关联是合适的。有关CallContext.HostContext的知识可以自行查阅资料,这里就不再赘述。
刚刚提到UnitOfWork模式,我们完成了DbContext的线程上下文内的唯一性,那么SaveChanges呢?嗯,我们可以基于之前的唯一性保证,来写一个SaveChanges的唯一入口。
public class DbSession
{public static int SaveChanges(){return DbContextFactory.GetDbContext().SaveChanges();}
}
End总结
本文简单介绍了CallContext类的基本概念、方法,做了一些测试验证了其提供的方法的适用范围和限制。
如果我们需要在.NET代码中向下传递对象,除了层层递进的传递参数之外,适时使用CallContext是一个不错的解耦的方案。
参考资料
Microsoft Doc,CallContext
.NET源码,https://referencesource.microsoft.com/#System.Web/HttpContext.cs
雯海,.NET多线程之CallContext(cnblogs博客)
Koma,EF上下文对象线程内唯一性与优化(csdn博客)
年终总结:Edison的2020年终总结
数字化转型:我在传统企业做数字化转型
C#刷题:C#刷剑指Offer算法题系列文章目录
.NET面试:.NET开发面试知识体系
.NET大会:2020年中国.NET开发者大会PDF资料
.NET | 多线程下的调用上下文 : CallContext相关推荐
- 多线程抢票_java多线程下模拟抢票
我们设置三个对象分别同时抢20张票,利用多线程实现. public class Web123506 implements Runnable{ private int ticteksNums=20;// ...
- Python多线程下调用win32com包相关问题:pywintypes.com_error: (-2147221008, ‘尚未调用 CoInitialize。‘, None, None)问题处理
报错1: pywintypes.com_error: (-2147221008, '尚未调用 CoInitialize.', None, None) 场景: 我是用 flask 服务操作接收的请求,通 ...
- C#多线程下, 子线程如何让主线程执行方法
C#多线程下, 子线程如何让主线程执行方法 重现一下当时我的需求: 我开了多个线程, 来监视一个变量, 然后去执行一些方法. 看起来没什么毛病, 但是运行起来会报错 此对象被其他线程占用 一开始想着可 ...
- springboot 多线程_SpringBoot异步调用@Async
一. 什么是异步调用? 异步调用是相对于同步调用而言的,同步调用是指程序按预定顺序一步步执行,每一步必须等到上一步执行完后才能执行,异步调用则无需等待上一步程序执行完即可执行. 二. 如何实现异步调用 ...
- java.text.SimpleDateFormat多线程下的问题
1. 今天在做性能压测的时候发现java.text.SimpleDateFormat多线程下的错误 2. 先贴出两段错误大家看一下: Exception in thread "pool-1- ...
- 多线程下HashMap的死循环
https://blog.csdn.net/dingjianmin/article/details/79780350 Java的HashMap是非线程安全的.多线程下应该用ConcurrentHash ...
- Java基础:详解HashMap在多线程下不安全
今天想知道HashMap为什么在多线程下不安全,找了许多资料,终于理解了. 首先先了解一下HashMap: HashMap实现的原理是:数组+链表 HashMap的size大于等于(容量*加载因子)的 ...
- 设计模式---单例模式(多线程下的单例模式)
1>单例类 package com.test.sigleton;public class SingletonTest {public static int num=0;//用于记录该类被实例化的 ...
- 调用lambda_如何使用Lambda调用上下文动态设置超时
调用lambda by Yan Cui 崔燕 如何使用Lambda调用上下文动态设置超时 (How to set timeouts dynamically using Lambda invocatio ...
最新文章
- 如何提升科研能力?以下这点最重要!
- 消除warning方法
- EasyUI中ToolTip提示框的简单使用
- Git根据远程分支建立条新的远程分支
- Vi(Linux系统下的标准编辑器)学习笔记
- c语言math函数 sgn,常用矩阵计算C语言代码
- thinkphp5.0自定义验证器
- Linux和windows下多线程的区别
- Html5+Css3小试牛刀
- 如何优化Flash动画使文件更小播放更流畅
- 聊天室私人聊天原理_如何设置极其安全的私人群组聊天
- C# 八种方案打印PDF文档
- 什么是DNS污染?DNS污染的解决方法
- 简单用 python 整一个 超级玛丽 小游戏 | 内附源码
- 2013年9月中秋云南昆明、丽江、泸沽湖、香格里拉、梅里雪山、虎跳峡之旅
- 计算机三级考点6:网络关键设备选型。
- raspios-bullseye-arm64 系统 BUG
- python写窗体程序_python写窗口
- DM36x 接入 AR0130 sensor
- 手机安装青龙面板,低功耗,随时随地的薅羊毛(无需服务器)
热门文章
- linux系统远程教程,Linux下实现远程协助
- delphi 算术溢出解决方法_性能优化系列:JVM 内存划分总结与内存溢出异常详解分析...
- Cordova入门系列(三)Cordova插件调用 转发 https://www.cnblogs.com/lishuxue/p/6018416.html...
- 上传jar包到nexus私服
- [Leetcode Week15]Populating Next Right Pointers in Each Node
- (十一)Jmeter另一种调试工具 HTTP Mirror Server
- Java设计模式----策略模式(Strategy)
- 【IBatisNet Spring.Net】ORM与IOC 简单配置
- 【转】SQL SERVER 存储过程学习笔记
- Linux操作系统下Sudo命令的使用方法说明