线程是操作系统能够进行运算调度的最小单位,操作系统线程进一步被封装成托管的Thread对象,手工创建并管理Thread对象已经成为了所能做到的对线程最细粒度的控制了。后来我们有了ThreadPool,可以更加方便地以池化的方式来使用线程。最后,Task诞生,它结合async/await关键字给与我们完美异步编程模式。但这一切让我们的编程体验越来越好,但是离线程的本质越来越远。被系列文章从“执行上下文传播”这个令开发者相对熟悉的角度来聊聊重新认识我们似乎已经很熟悉的主题。

目录

一、ThreadStatic字段或者ThreadLocal<T>对象

二、CallContext

三、支持跨线程传递吗?

四、IllogicalCallContext和LogicalCallContext

五、AsyncLocal<T>

一、ThreadStatic字段或者ThreadLocal<T>对象

本篇文章旨在解决一个问题:对于一个由多个方法组成的调用链,数据如何在上下游方法之间传递。我想很多人首先想到的就是通过方法的参数进行传递,但是作为方法签名重要组成部分的参数列表代表一种“契约”,往往是不能轻易更改的。既然不能通过参数直接进行传递,那么我们需要一个“共享”的数据容器,上游方法将需要传递的数据放到这个容器中,下游方法在使用的时候从该容器中将所需的数据提取出来。

那么这个共享的容器可以是一个静态字段,当然不行, 因为类型的静态字段类似于一个单例对象,它会被多个并发执行的调用链共享。虽然普通的静态字段不行,但是标注了ThreadStaticAttribute特性的静态字段则可以,因为这样的字段是线程独享的。为了方便演示,我们定义了如下一个CallStackContext类型来表示基于某个调用链的上下文,这是一个字典,用于存放任何需要传递的数据。自增的TraceId字段代码当前调用链的唯一标识。当前的CallStackContext上下文通过静态属性Current获取,可以看出它返回标注了ThreadStaticAttribute特性的静态字段_current。

public class CallStackContext : Dictionary<string, object>
{
[ThreadStatic]
private static CallStackContext _current;
private static int _traceId = 0;
public static CallStackContext Current { get => _current; set => _current = value; }
public long TraceId { get; } = Interlocked.Increment(ref _traceId);
}

我们通过如下这个CallStack对象创建一个“逻辑”上的调用链。在初始化的时候,CallStack会创建一个CallStackContext对象并将其放进CallContext对象并对静态字段_current进行复制。该字段会在Dispose方法中被置空,此时标志逻辑调用链生命周期的终止。

public class CallStack : IDisposable
{
public CallStack() => CallStackContext.Current = new CallStackContext();
public void Dispose() => CallStackContext.Current = null;
}

我们通过如下的程序来演示针对CallStack和CallStackContext的使用。如代码片段所示,我们利用对象池并发调用Call方法。Call方法内部会依次调用Foo、Bar和Baz三个方法,需要传递的数据体现为一个Guid,我们将当存放在当前CallStackContext中。整个方法Call方法的操作均在创建Callback的using block中执行。

class Program
{
static void Main()
{
for (int i = 0; i < 5; i++)
{
ThreadPool.QueueUserWorkItem(_ => Call());
}
Console.Read();
}
static void Call()
{
using (new CallStack())
{
CallStackContext.Current["argument"] = Guid.NewGuid();
Foo();
Bar();
Baz();
}
}
static void Foo() => Trace();
static void Bar() => Trace();
static void Baz() => Trace();
static void Trace([CallerMemberName] string methodName = null)
{
var threadId = Thread.CurrentThread.ManagedThreadId;
var traceId = CallStackContext.Current?.TraceId;
var argument = CallStackContext.Current?["argument"];
Console.WriteLine($"Thread: {threadId}; TraceId: {traceId}; Method: {methodName}; Argument:{argument}");
}
}

为了验证三个方法获取的数据是否正确,我们让它们调用同一个Trace方法,该方法会在控制台上打印出当前线程ID、调用链标识(TraceId)、方法名和获取到的数据。如下所示的是该演示程序执行后的结果,可以看出置于CallContext中的CallStackContext对象帮助我们很好地完成了针对调用链的数据传递。

既然我们可以使用ThreadStatic静态字段,自然也可以使用ThreadLocal<T>对象来代替。如果希望时候后者,我们只需要将CallStackContext改写成如下的形式即可。

public class CallStackContext : Dictionary<string, object>
{
private static ThreadLocal<CallStackContext> _current = new ThreadLocal<CallStackContext>();
private static int _traceId = 0;
public static CallStackContext Current { get => _current.Value; set => _current.Value = value; }
public long TraceId { get; } = Interlocked.Increment(ref _traceId);
}

二、CallContext

除使用ThreadStatic字段来传递调用链数据之外,我们还可以使用CallContext。顾名思义,CallContext是专门为调用链创建的上下文,我们首先利用它来实现基于调用链的数据传递。如果采用这种解决方案,上述的CallStack和CallStackContext类型可以改写成如下的形式。如代码片段所示,当前的CallStackContext上下文通过静态属性Current获取,可以看出它是通过调用CallContext的静态方法GetData提取的,传入的类型名称作为存放“插槽”的名称。在初始化的时候,CallStack会创建一个CallStackContext对象并将其放进CallContext对应存储插槽中作为当前上下文,该插槽会在Dispose方法中被释放

public class CallStackContext: Dictionary<string, object>
{
private static int _traceId = 0;
public static CallStackContext Current => CallContext.GetData(nameof(CallStackContext)) as CallStackContext;
public long TraceId { get; } = Interlocked.Increment(ref _traceId);
}

public class CallStack : IDisposable
{
public CallStack() => CallContext.SetData(nameof(CallStackContext), new CallStackContext());
public void Dispose() => CallContext.FreeNamedDataSlot(nameof(CallStackContext));
}

三、支持跨线程传递吗?

对于上面演示的实例来说,调用链中的三个方法(Foo、Bar和Baz)均是在同一个线程中执行的,如果出现了跨线程调用,CallContext是否还能帮助我们实现上下文的快线程传递吗?为了验证CallContext跨线程传递的能力,我们将Call方法改写成如下的形式:Call方法直接调用Foo方法,但是Foo方法针对Bar方法的调用,以及Bar方法针对Baz方法的调用均在一个新创建的线程中进行的。

static void Call()
{
using (new CallStack())
{
CallStackContext.Current["argument"] = Guid.NewGuid();
Foo();
}
}
static void Foo()
{
Trace();
new Thread(Bar).Start();
}
static void Bar()
{
Trace();
new Thread(Baz).Start();
}
static void Baz() => Trace();

再次执行我们我们的程序,不论是采用基于ThreadStatic静态字段,还是采用ThreadLocal<T>对象或者CallContext的解决方法,均会得到如下所示的输出结果。可以看出设置的数据只能在Foo方法中获取到,但是并没有自动传递到异步执行的Bar和Baz方法中。

四、IllogicalCallContext和LogicalCallContext

其实CallContext设置的上下文对象分为IllogicalCallContext和LogicalCallContext两种类型,调用SetData设置的是IllogicalCallContext,它并不具有跨线程传播的能力。如果希望在进行异步调用的时候自动传递到目标线程,必须调用CallContext的LogicalSetData方法设置为LogicalCallContext。所以我们应该将CallStack类型进行如下的改写。

public class CallStack : IDisposable
{
public CallStack() => CallContext.LogicalSetData(nameof(CallStackContext), new CallStackContext());
public void Dispose() => CallContext.FreeNamedDataSlot(nameof(CallStackContext));
}

与之相对,获取LogicalCallContext对象的方法也得换成LogicalGetData,为此我们将CallStackContext改写成如下的形式。

public class CallStackContext: Dictionary<string, object>
{
private static int _traceId = 0;
public static CallStackContext Current => CallContext.LogicalGetData(nameof(CallStackContext)) as CallStackContext;
public long TraceId { get; } = Interlocked.Increment(ref _traceId);
}

再次执行我们程序,依然能够得到希望的结果。

除了将设置和提取当前CallStackContext的方式进行修改(GetData=>LogicalGet; SetData=>LogicalSetData)之外,我们还有另一个解决方案,那就是让放存放在CallContext存储槽的数据类型实现ILogicalThreadAffinative接口。该接口没有定义任何成员,实现类型对应的对象将自动视为LogicalCallContext。对于我们的演示实例来说,我们只需要让CallStackContext实现该接口就可以了。

public class CallStackContext: Dictionary<string, object>, ILogicalThreadAffinative
{
private static int _traceId = 0;
public static CallStackContext Current => CallContext.GetData(nameof(CallStackContext)) as CallStackContext;
public long TraceId { get; } = Interlocked.Increment(ref _traceId);
}

五、AsyncLocal<T>

CallContext并没有被.NET Core继承下来。也就是,只有.NET Framework才提供针对CallContext的支持,.因为我们有更好的选择,那就是AsyncLocal<T>。如果使用AsyncLocal<T>作为存放调用链上下文的容器,我们的

public class CallStackContext: Dictionary<string, object>, ILogicalThreadAffinative
{
internal static readonly AsyncLocal<CallStackContext> _contextAccessor = new AsyncLocal<CallStackContext>();
private static int _traceId = 0;
public static CallStackContext Current => _contextAccessor.Value;
public long TraceId { get; } = Interlocked.Increment(ref _traceId);
} public class CallStack : IDisposable
{
public CallStack() => CallStackContext._contextAccessor.Value = new CallStackContext();
public void Dispose() => CallStackContext._contextAccessor.Value = null;
}

既然命名为AsyncLocal<T>,自然是支持异步调用。它不仅支持上面演示的直接创建线程的方式,最主要的是支持我们熟悉的await的方式(如下所示)。

class Program
{
static async Task Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
ThreadPool.QueueUserWorkItem(_ => Call());
}
Console.Read();
Console.Read(); async Task Call()
{
using (new CallStack())
{
CallStackContext.Current["argument"] = Guid.NewGuid();
await FooAsync();
await BarAsync();
await BazAsync();
}
}
}
static Task FooAsync() => Task.Run(() => Trace());
static Task BarAsync() => Task.Run(() => Trace());
static Task BazAsync() => Task.Run(() => Trace());
static void Trace([CallerMemberName] string methodName = null)
{
var threadId = Thread.CurrentThread.ManagedThreadId;
var traceId = CallStackContext.Current?.TraceId;
var argument = CallStackContext.Current?["argument"];
Console.WriteLine($"Thread: {threadId}; TraceId: {traceId}; Method: {methodName}; Argument:{argument}");
}
}

从执行上下文角度重新理解.NET(Core)的多线程编程[1]:基于调用链的”参数”传递的更多相关文章

  1. 从执行上下文角度重新理解.NET(Core)的多线程编程[2]:同步上下文

    一般情况下,我们可以将某项操作分发给任意线程来执行,但有的操作确实对于执行的线程是有要求的,最为典型的场景就是:GUI针对UI元素的操作必须在UI主线程中执行.将指定的操作分发给指定线程进行执行的需求 ...

  2. 理解和使用NT驱动程序的执行上下文

    理解Windows NT驱动程序最重要的概念之一就是驱动程序运行时所处的“执行上下文”.理解并小心地应用这个概念可以帮助你构建更快.更高效的驱动程序. NT标准内核模式驱动程序编程的一个重要观念是某个 ...

  3. 深入理解JavaScript执行上下文、函数堆栈、提升的概念

    本文内容主要转载自以下两位作者的文章,如有侵权请联系我删除: https://feclub.cn/post/content/ec_ecs_hosting http://blog.csdn.net/hi ...

  4. javascript系列之执行上下文

    原文:javascript系列之执行上下文 写在前面:一 直想系统的总结一下学过的javascript知识,喜欢这门语言也热爱这门语言.未来想从事前端方面的工作,提前把自己的知识梳理一下.前面写了些 ...

  5. 你不知道的JavaScript--Item19 执行上下文(execution context)

    在这篇文章里,我将深入研究JavaScript中最基本的部分--执行上下文(execution context).读完本文后,你应该清楚了解释器做了什么,为什么函数和变量能在声明前使用以及他们的值是如 ...

  6. 通俗易懂的来讲讲js的函数执行上下文

    0.开场白 在平时编写JavaScript代码时,我们并不会和执行上下文直接接触,但是想要彻底搞懂JavaScript函数的话,执行上下文是我们绕不过去的一个知识点. 1.执行上下文栈 JavaScr ...

  7. JS进阶系列之执行上下文

    function test(){ console.log(a);//undefined; var a = 1; } test(); 也许你会遇到过上面这样的面试题,你只知道它考的是变量提升,但是具体的 ...

  8. js基础梳理-究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?

    日常在群里讨论一些概念性的问题,比如变量提升,作用域和闭包相关问题的时候,经常会听一些大佬们给别人解释的时候说执行上下文,调用上下文巴拉巴拉,总有点似懂非懂,不明觉厉的感觉.今天,就对这两个概念梳理一 ...

  9. this以及执行上下文概念的重新认识

    在理解this的绑定过程之前,必须要先明白调用位置,调用位置指的是函数在代码中被调用的位置,而不是声明所在的位置. (ES6的箭头函数不在该范围内,它的this在声明时已经绑定了,而不是取决于调用时. ...

随机推荐

  1. Linux入门到放弃之六《磁盘和文件系统管理三》

    设置磁盘配额 对之前创建的逻辑卷设置磁盘配额,要求用户student对该逻辑卷 容量的软限制是:5G,硬限制是7G,文件个数软限制为:25个,硬限制为30个. (1)首先对/etc/fstab文件进行 ...

  2. [Luogu P1613]跑路 (DP+倍增+最短路)

    题面 传送门:https://www.luogu.org/problemnew/show/P1613 Solution 挺有意思的一道题. 题面已经挺明显的描述出了这题的主要思想:倍增. 先这样想,我 ...

  3. [BZOJ 4818/LuoguP3702][SDOI2017] 序列计数 (矩阵加速DP)

    题面: 传送门:https://www.lydsy.com/JudgeOnline/problem.php?id=4818 Solution 看到这道题,我们不妨先考虑一下20分怎么搞 想到暴力,本蒟 ...

  4. kafka的基本安装与使用

    kafka的基本安装与使用 1.上官网下载tar包 2.解压 3.运行zookeeper 4.运行kafka服务器 5.创建topic 6.发送消息 7.监听消息 1.上官网下载tar包 https: ...

  5. XJOI 夏令营501-511测试11 统计方案

    小B写了一个程序,随机生成了n个正整数,分别是a[1]..a[n],他取出了其中一些数,并把它们乘起来之后模p,得到了余数c.但是没过多久,小B就忘记他选了哪些数,他想把所有可能的取数方案都找出来.你 ...

  6. dhcp、tftp、httpd、pxe安装CentOS6.9

    虚拟机网络设置 要xshell连接虚拟机注意设置VMware Network Adapter VMnet2在同一网段 1.利用光盘配置本地yum源 [root@ZYB ~]# mount -r /de ...

  7. 制作安装Linux操作系统的启动U盘方法其实就这么简单

    安装系统前,我们需要制作系统安装启动U盘(或者DVD,但已经很少使用DVD,所以不再介绍),通过启动U盘来帮助我们安装下载好的Linux系统. 接下来我们来介绍在Windows.Linux和MacOS ...

  8. 说说 C# 9 新特性的实际运用

    你一定会好奇:"老周,你去哪开飞机了?这么久没写博客了." 老周:"我买不起飞机,开了个铁矿,挖了一年半的石头.谁知铁矿垮了,压死了几条蜈蚣,什么也没挖着." ...

  9. tensorflow的广播机制

    TensorFlow支持广播机制(Broadcast) TensorFlow支持广播机制(Broadcast),可以广播元素间操作(elementwise operations).正常情况下,当你想要 ...

  10. jquery自定义弹层显示大图(兼容多层iframe)

    1.介绍 a:可用于多层iframe中,显示在最外层 b:动画效果为从点击的图片位置开始放大至全屏显示 2.效果图 3.js代码 1 function ShowMaxImg(src, y, x, w, ...