最近在分析现在团队的项目代码(基于.NET Framework 4.5),经常发现一个CallContext的调用,记得多年前的时候用到了它,但是印象已经不深刻了,于是现在来复习一下。

1 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

 

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;
}

4 EF 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();
}
}

5 总结

本文简单介绍了CallContext类的基本概念、方法,做了一些测试验证了其提供的方法的适用范围和限制。

如果我们需要在.NET代码中向下传递对象,除了层层递进的传递参数之外,适时使用CallContext是一个不错的解耦的方案。

参考资料

Microsoft Doc,CallContext

.NET源码,https://referencesource.microsoft.com/#System.Web/HttpContext.cs

雯海,.NET多线程之CallContext(cnblogs博客)

Koma,EF上下文对象线程内唯一性与优化(csdn博客)

作者:周旭龙

出处:https://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

多线程下的调用上下文 : CallContext的更多相关文章

  1. 一个实现单线程/多线程下代码调用链中传递数据的处理类: CallContext(LogicalSetData,LogicalGetData),含.net core的实现

    详情请参考原文:一个实现单线程/多线程下代码调用链中传递数据的处理类: CallContext

  2. .NET多线程之调用上下文CallContext

    命名空间:System.Runtime.Remoting.Messaging 类型完全限定名称:System.Runtime.Remoting.Messaging.CallContext 官方介绍:h ...

  3. java多线程下如何调用一个共同的内存单元(调用同一个对象)

    /* * 关于线程下共享相同的内存单元(包括代码与数据) * ,并利用这些共享单元来实现数据交换,实时通信与必要的同步操作. * 对于Thread(Runnable target)构造方法创建的线程, ...

  4. CallContext线程数据缓存-调用上下文

    一.CallContext 概述 命名空间:System.Runtime.Remoting.Messaging CallContext 用于提供与执行代码路径一起传送的属性集,直白讲就是:提供线程(多 ...

  5. ASP.NET多线程下使用HttpContext.Current为null解决方案 2015-01-22 15:23 349人阅读 评论(0) 收藏

    问题一:多线程下获取文件绝对路径 当我们使用HttpContext.Current.Server.MapPath(strPath)获取绝对路径时HttpContext.Current为null,解决办 ...

  6. ASP.NET多线程下使用HttpContext.Current为null解决方案 2015-01-22 15:23 350人阅读 评论(0) 收藏

    问题一:多线程下获取文件绝对路径 当我们使用HttpContext.Current.Server.MapPath(strPath)获取绝对路径时HttpContext.Current为null,解决办 ...

  7. ASP.NET多线程下使用HttpContext.Current为null解决方案

    多线程或者异步调用中如何访问HttpContext? 前面我还提到在APM模式下的异步完成回调时,访问HttpContext.Current也会返回null,那么此时该怎么办呢? 答案有二种:1. 在 ...

  8. .NET:线程本地存储、调用上下文、逻辑调用上下文

    .NET:线程本地存储.调用上下文.逻辑调用上下文 目录 背景线程本地存储调用上下文逻辑调用上下文备注 背景返回目录 在多线程环境,如果需要将实例的生命周期控制在某个操作的执行期间,该如何设计?经典的 ...

  9. Java进阶专题(十五) 从电商系统角度研究多线程(下)

    前言 ​ 本章节继上章节继续梳理:线程相关的基础理论和工具.多线程程序下的性能调优和电商场景下多线程的使用. 多线程J·U·C ThreadLocal 概念 ​ ThreadLocal类并不是用来解决 ...

  10. C#多线程下的调优

    一.原子操作 先看一段问题代码 /// <summary> /// 获取自增 /// </summary> public static void GetIncrement() ...

随机推荐

  1. 方法重写-java se 进阶-day01

    1.方法重写的介绍 当子父类中,某方法存在相同的定义(方法名.参数.返回值)时,子类的方法会将父类的方法进行重写操作(覆盖) 2.方法重写与方法重载的区别 1.方法重载:又称Overload,在同一个 ...

  2. Javascript try catch finally

    很少用到javascript的异常捕获,突然需要,发现挺全的,小小记录下,留爪. 直接打码: //public.js //异常输出 function exception(ex) { console.l ...

  3. lua三色标记的读写屏障理解

    起因是已经被标记为黑色的对象无法进行再次遍历,然而黑色对象发生了引用变化:断开了引用或者引用了别的对象,会导致多标(不再被黑色对象引用的对象未能回收),漏标(黑色对象的新引用未能遍历标记)

  4. 使用Python可视化偶极子的电场

    引言 在电学中,偶极子是一个非常重要且有趣的概念.它由两个电荷(一个正电荷和一个负电荷)组成,并且这两个电荷具有相同的大小和相反的符号.偶极子的电场分布具有独特的特点,能够帮助我们深入理解电场的性质. ...

  5. vue报错:Property or method "xxx" is not defined on the instance but referenced during render.

    vue报错:Property or method "attendanceDetaill" is not defined on the instance but referenced ...

  6. PHP获取一个月所有时间

    $j = date("t"); //获取当前月份天数$start_time = strtotime(date('Y-m-01')); //获取本月第一天时间戳$array = ar ...

  7. Spring 基于注解的AOP面向切面编程

    Spring 基于注解的AOP面向切面编程 源码 代码实现 pom.xml <?xml version="1.0" encoding="UTF-8"?&g ...

  8. packer 学习笔记

    前言 网上有一个老哥用 packer 制作镜像的博客里开篇就提到[1]. Failure is success in progress. -- Albert Einstein 不要害怕失败,在用 pa ...

  9. 盈亏平衡之"盈亏平衡点和总可变成本和总收入和利润和边际收益"

    案例1 案例2 案例3 因为他这里没有按2w件来算,而是按4w件 利润 = 总收入 - 总成本 总收入 = 产量 * 单价 总成本 = 固定成本 + 变动成本 变动成本 = 单件可变成本 * 产量 案 ...

  10. 作业时间之"最早时间和最晚时间"

    一.从左往右(小到大)算最早时间 0+2=2 0+3=3 因为3比2大所以选择3(早大晚小),需活动无时间所以不用加 3+4=7 7+3=10 因为13号点有两个 2+5=7 和 11号点的10(虚活 ...