.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资料

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/301284.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

每日一笑 | 哪个男人到底是谁?!

全世界只有3.14 % 的人关注了数据与算法之美&#xff08;图源网络&#xff0c;侵权删&#xff09;

min里所有的参数都不存在_高中生物所有的考点难点,其实都在你不仔细看的课本里,必修1-3超强记忆手册!...

对很多理科生来说&#xff0c;高中生物就是一门不是文科胜似文科的学科。很多数学、物理成绩非常突出的学生却不能在这样一门“理科”课程当中取得优势。生物老师在这门学科的提高上反复强调“回归课本”却又让很多习惯刷题的理科生不知无从下手。进入高三后&#xff0c;生物、…

我看电商(作者近三十年从事零售及电子商务管理的总结和分享)

我看电商&#xff08;作者近三十年从事零售及电子商务管理的总结和分享&#xff09; 黄若 著 ISBN 978-7-121-20268-1 2013年6月出版 定价&#xff1a;39.00元 284页 16开 编辑推荐 近年来电商行业在中国迅猛发展&#xff0c;电子商务正在日益深入的影响着越来越多人的生活。…

每日一笑 | 坐牢吗?学编程那种~

全世界只有3.14 % 的人关注了数据与算法之美&#xff08;图源网络&#xff0c;侵权删&#xff09;

VMware vSphere 5.1 群集深入解析(二十六)- 数据存储维护模式汇总

VMware vSphere5.1Clustering DeepdiveHA.DRS.Storage DRS.Stretched ClustersDuncan Epping &Frank DennemanTranslate By Tim2009 / 翻译&#xff1a;Tim2009目录版权关于作者知识点前言第一部分 vSphere高可用性第一章 介绍vSphere高可用性第二章 高可用组件第三章 基本…

硕士论文研究「AI预测性取向」:化妆等因素并不影响判断

全世界只有3.14 % 的人关注了数据与算法之美2017 年&#xff0c;斯坦福大学的一篇《深度学习通过面部识别判断性取向超越人类》曾引发了极大争议&#xff0c;其通过 AI 算法仅需「看面相」即可判断一个人是不是同性恋的方法让众人感到一丝恐慌&#xff0c;也让技术研究者们对于…

[翻译]Go与C#对比 第三篇:编译、运行时、类型系统、模块和其它的一切

Go vs C#, Part 3: Compiler, Runtime, Type System, Modules, and Everything Else | by Alex Yakunin | ServiceTitan — Titan Tech | Medium目录译者注相似性编译垃圾回收模块类、结构、接口错误处理相等性&#xff08;, !&#xff09;基础类库两种语言中存在的其他类似特…

指针易出错点一

2019独角兽企业重金招聘Python工程师标准>>> 声明指针中会遇到的问题 int* p1,p2; 上面这条语句的本意应该是声明两个指向int的指针&#xff0c;而实际的效果是p1是指针类型&#xff0c;p2却是int类型&#xff0c;这是因为在C语言中&#xff0c;声明和解释的语法并…

206块积木,72套进阶玩法!玩转STEAM教育,帮你省掉上万块的乐高课

▲数据汪特别推荐点击上图进入玩酷屋作为一名资深积木达人&#xff0c;小木我可是大大小小的积木阅览无数&#xff0c;当然乐高也不会放过&#xff0c;虽然“钱包君”已经是路人了。&#xff08;每月的工资用来买乐高~&#xff09;之前给大家推荐了一款STEM积木&#xff0c;小小…

python如何正则匹配浮点值_python使用正则搜索字符串或文件中的浮点数代码实例...

# -*- coding: utf-8 -*-#----------------------------------------------------------------------# FileName:gettxtdata.py#功能:读取字符串和文件中的数值数据(浮点数)#主要提供类似matlab中的dlmread和dlmwrite函数#同时提供loadtxtdata和savetxtdata函数#Data: 2013-1-1…

用了VS2022,你可能再也回不去旧版!

VS2022发布第11天&#xff0c;最直接的使用感受就是智能提示太哇塞了&#xff0c;之前是提示一个单词&#xff0c;现在直接提示一行&#xff0c;撸码速度直接起飞&#xff01;再就是打开ASP.NET Core的源码&#xff0c;500多个项目3G的体积&#xff0c;过程比VS2019要顺滑多了&…

poj2578

简单题 #include <cstdio>int main() {int f[5];for (int i 0; i < 3; i){scanf("%d", &f[i]);if (f[i] < 168){printf("CRASH %d\n", f[i]);return 0;}}printf("NO CRASH\n");return 0; } View Code 转载于:https://www.cnblo…

意大利归还中国文物;翟天临咪蒙成考公务员题目;携程回应五一机票涨价;腾讯未成年人网络保护体系上线;这就是今天的大新闻...

今天是3月25日农历二月十九今天星期一整个周末都贡献给都挺好了下面是今天的大新闻意大利归还796件中国文物&#xff08;新京报&#xff09;3月23日&#xff0c;在中国主席和意大利总理孔特共同见证下&#xff0c;中意双方代表交换关于796件套中国流失文物艺术品返还的证书。这…

Hello Blazor:(1)像ASP.NET WebForm一样写代码

写在前面最近&#xff0c;刚开始学习Blazor。对于后端出身的程序员来说&#xff0c;使用Blazor上手开发前端程序&#xff0c;门槛确实降低了不少。还在观望的朋友可以尝试入坑了。在学习和编写Blazor程序的过程中&#xff0c;我产生了一些想法&#xff0c;将会逐渐在本系列中呈…

VScode设置背景颜色

1.打开VSCode,点击左上角文件选项 2.点击首选项 3.点击颜色主题 4.使用键盘上下方向键即可选择不同颜色的背景

选择比努力更重要,这些微信号值得你细细品读。

一个人无论是平凡的还是不平凡的只是自自然然地按照自己喜欢的样子去生活这是最省力且最快乐的人生选择以下几个优质公众号能让你在闲暇的时候不断的提升自我&#xff0c;拓宽视野愿以书卷气&#xff0c;行我路千里长按二维码&#xff0c;选择【识别图中二维码】关注少年数学家…

ASP.NET Core 6 Minimal API

ASP.NET Core 6 Minimal APIIntro微软在 ASP.NET 6 Preview 4 的介绍文章中介绍了即将到来的 ASP.NET Core 6 中的最小 API 的雏形&#xff0c;我们现在已经基本可以达到最小化 API 了雏形了&#xff0c;在 Preview 4 的时候就写了这个小示例&#xff0c;但是不够简洁&#xff…

支付宝今日起还卡收费;大城市女性买房猛增;小米发布100W快充技术;严查非法办学行为;交大通报博导辱骂学生;这就是今天的大新闻...

今天是3月26日农历二月二十今天星期二不是个让人可以昏昏欲睡的日子下面是今天的大新闻支付宝还信用卡开始收费&#xff08;澎湃新闻&#xff09;根据支付宝此前发布的消息&#xff0c;自3月26日起&#xff0c;通过支付宝给信用卡还款超过免费额度后将收取0.1%的服务费&#xf…

WPF任务栏同步进度

一、概要本篇文章主要分享使用TaskbarItemInfo对象&#xff08;WPF&#xff09;在window操作系统的任务栏中同步任务进度的功能。什么是TaskbarItemInfo对象&#xff1f;TaskbarItemInfo类为 Windows 7 任务栏功能提供托管包装。有关 Windows shell 和本机任务栏 Api 的详细信息…

每日一笑 | 各大互联网公司离职员工群名

全世界只有3.14 % 的人关注了数据与算法之美&#xff08;图源网络&#xff0c;侵权删&#xff09;