CodeSnippet.Cn
代码片段
Csharp
架构设计
.NetCore
西班牙语
kubernetes
MySql
Redis
Algorithm
Other
Ubuntu
Linux
.NetMvc
VisualStudio
Python
Git
pm
WPF
java
Plug-In
分布式
CSS
微服务架构
JavaScript
DataStructure
Shared
.NET CallContext在多线程上下文环境进行数据传递
0
.NetCore
Csharp
小笨蛋
发布于:2021年06月26日
更新于:2021年06月26日
137
#custom-toc-container
最近在分析现在团队的项目代码(基于.NET Framework 4.5),经常发现一个`CallContext`的调用,记得多年前的时候用到了它,但是印象已经不深刻了,于是现在来复习一下。 ### CallContext是个啥? 如果说,一个对象保证全局唯一,大家肯定会想到一个经典的设计模式:单例模式。但是,如果要使用的对象必须是线程内唯一的呢? 在.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 | - | ### 探究CallContext方法 上面介绍了`CallContext`提供的核心方法,下面我们就来通过实践来理解一下。 #### 准备工作 这里准备一个`User`类作为数据传递对象: ```csharp public class User { public string Id { get; set; } public string Name { get; set; } } ``` **测试1:GetData、SetData 与 FreeNamedDataSlot** 测试代码很简单,就是在主线程 和 子线程之中分别传递`User`对象实例,看看最后的效果。 ```csharp public void TestGetSetData() { // 主线程执行 Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}"); var user = new User() { Id = DateTime.Now.ToString(), Name = "代码片段CodeSnippet.cn" }; 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()); } ``` 上面示例代码的运行结果如下图所示: ```shell //主线程 Current ThreadId=1 True Current ThreadId=1 True NULL //子线程 Current ThreadId=3 NULL ``` 根据上面所示的结果,基本可以得出以下两个结论: 1. **`GetData`、`SetData`方法只能用于单线程环境**,如果发生了线程切换,存储的数据也会随之丢失。 2. **`GetData` 和 `SetData` 可以用于同一线程中的不同地方,传递数据。** 可以知道,要在多线程环境下使用,我们需要用到另外两个方法:`LogicalSetData` 与 `LogicalGetData`。 **测试2:LogicalGetData、LogicalSetData 与 FreeNamedDataSlot** ```csharp public void TestLogicalGetSetData() { // 主线程执行 Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}"); var user = new User() { Id = DateTime.Now.ToString(), Name = "代码片段CodeSnippet.cn" }; 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()); } ``` 这段示例代码的运行结果如下面所示: ```shell Current ThreadId=1 True Current ThreadId=1 NULL //不影响 Current ThreadId=3 True True ``` 根据上面所示的结果,基本可以得出以下三个结论: 1. **`FreeNamedDataSlot`只能清除当前线程的数据槽**,不能清除子线程的数据槽; 2. **`LogicalSetData`、`LogicalGetData`可用于在多线程环境下传递数据**; 3. `FreeNamedDataSlot`清除当前线程的数据槽后,**之前已经运行的子任务,不受影响**; **测试3:LogicalGetData后修改传递的数据** 在多线程环境下传递共享对象数据,如果某个线程通过`LogicalGetData`后对其进行了修改又重新`LogicalSetData`会怎样? ```csharp public static void TestLogicalGetSetDataV2() { // 主线程执行 CodeSnippet.cn Console.WriteLine($"Current ThreadId={Thread.CurrentThread.ManagedThreadId}"); var user = new User() { Id = DateTime.Now.ToString(), Name = "代码片段CodeSnippet.cn" }; 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 = "片段"; CallContext.LogicalSetData("key", new User() { Id = DateTime.Now.ToString(), Name = "代码" }); // 只影响当前线程 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}"); } ``` 上面示例代码的运行结果如下面所示: ```shell Current ThreadId=1 True Current ThreadId=3 True NULL Current ThreadId=3 True False User.Name=代码 Current ThreadId=1 True //子线程修改后,父线程读到了 User.Name=片段 ``` 根据上面的示例运行结果,我们又可以得到以下一些结论: 1. **`FreeNamedDataSlot`只能清除当前线程的数据槽**; 2. **`LogicalSetData`只会存储当前线程以及子线程的数据槽**; 3. **`LogicalGetData`获取的是当前线程或父线程的数据槽对象,拿到的是对象的引用**,因此如果对其进行修改,会影响父线程读取的一致性,在关系型数据库中也被称为不可重复读。 4. **子线程中使用`LogicalSetData`改变数据槽的值,不会影响父线程的数据槽,即使他们的key是同一个**; ### .NET Core下没有CallContext 在.NET Core下没有`CallContext`类,取而代之的是使用`AsyncLocal`代替,实现的是`CallContext.LogicalGetData` 和 `CallContext.SetLogicalCallContext` 例如,下面是一个示例代码,我们可以借助`AsyncLocal`来自己实现一个`CallContext`类。如果你是将.NET Framework升级为.NET Core,那么你可能需要自己实现一个`CallContext`类来代替之前的`CallContext`: ```csharp public static class CallContext { static ConcurrentDictionary
> state = new ConcurrentDictionary
>(); public static void SetData(string name, object data) => state.GetOrAdd(name, _ => new AsyncLocal
()).Value = data; public static object GetData(string name) => state.TryGetValue(name, out AsyncLocal
data) ? data.Value : null; } ``` ### EF DbContext场景 对于像`UnitOfWork`这种操作模式,是比较适合于`CallContext`发挥的地方,让`EF DbContext`在线程上下文内保持唯一。 > 注意:这里提到的EF均指EF 而非 EF Core。 因此,我们经常可以看到如下所示的示例代码: ```csharp 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`: ```csharp 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`实现的。 ```csharp 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`的唯一入口。 ```csharp public class DbSession { public static int SaveChanges() { return DbContextFactory.GetDbContext().SaveChanges(); } } ``` ### 总结 本文简单介绍了`CallContext`类的基本概念、方法,做了一些测试验证了其提供的方法的适用范围和限制。 如果我们需要在.NET代码中向下传递对象,除了层层递进的传递参数之外,适时使用`CallContext`是一个不错的解耦的方案。
这里⇓感觉得写点什么,要不显得有点空,但还没想好写什么...
返回顶部
About
京ICP备13038605号
© 代码片段 2025