【橙子老哥】c# AsyncLocal底层原理

hello,大家好,又到了橙子老哥的分享时间,希望大家一起学习,一起进步。

欢迎加入.net意社区,第一时间了解我们的动态,地址:ccnetcore.com

废话少说,我们直接开始

1、ThreadLocal与AsyncLocal

众所皆知,AsyncLocal是用于异步方法之间的数据隔离,而 ThreadLocal是用于多线程之间的数据隔离,需要明白,多线程 != 异步,多线程只是异步的一种实现,两者完全不是同一水平的东西,不能进行比较

关于他们的区别,相信大家看过很多的文章了,我总结放两个例子,不多赘述,带过即可

AsyncLocal<Student> context =new AsyncLocal<Student>();
await Task.Run(async () =>
{
    context.Value = new Student { Name = $"张三" };
    Console.WriteLine($"值:{context.Value?.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay(1000);
    Console.WriteLine($"值:{context.Value?.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
    return Task.CompletedTask;
});
输出结果:(正确)
值:张三,ThreadId=9
值:张三,ThreadId=7

如果改为:

ThreadLocal<Student> context =new ThreadLocal<Student>();
await Task.Run(async () =>
{
    context.Value = new Student { Name = $"张三" };
    Console.WriteLine($"值:{context.Value?.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay(1000);
    Console.WriteLine($"值:{context.Value?.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
    return Task.CompletedTask;
});
输出结果:(错误)
值:张三,ThreadId=6 (随缘)
值:,ThreadId=8

2、探究AsyncLocal原理

又到了大家最爱的探究环境,让我们深入看看AsyncLocal的源码
最外层的源码不多:

        [MaybeNull]
        public T Value
        {
            get
            {
                object? value = ExecutionContext.GetLocalValue(this);
                if (typeof(T).IsValueType && value is null)
                {
                    return default;
                }

                return (T)value!;
            }
            set
            {
                ExecutionContext.SetLocalValue(this, value, _valueChangedHandler is not null);
            }
        }

主要是一个get和set,对应的就是value方法的赋值和查询

按照以往惯例,get方法,ExecutionContext.GetLocalValue(this)肯定很简单,不出意外:

 internal static object? GetLocalValue(IAsyncLocal local)
        {
            ExecutionContext? current = Thread.CurrentThread._executionContext;
            if (current == null)
            {
                return null;
            }

            Debug.Assert(!current.IsDefault);
            Debug.Assert(current.m_localValues != null, "Only the default context should have null, and we shouldn't be here on the default context");
            current.m_localValues.TryGetValue(local, out object? value);
            return value;
        }

只是从current.m_localValues中根据IAsyncLocal的引用,获取到值而已

那么我们想要追踪到源头,就要看current.m_localValues的值怎么给进去的了,我们看看set

 //设置值的方法
    internal static void SetLocalValue(IAsyncLocal local, object? newValue, bool needChangeNotifications)
    {
        ExecutionContext? current = Thread.CurrentThread._executionContext;
        //判断设置的心值和旧值是否相同
        object? previousValue = null;
        bool hadPreviousValue = false;
        if (current != null)
        {
            hadPreviousValue = current.m_localValues.TryGetValue(local, out previousValue);
        }
        //相同的话不在进行设置直接返回
        if (previousValue == newValue)
        {
            return;
        }

        if (current != null)
        {
            //设置新值
            newValues = current.m_localValues.Set(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);
        }
        else
        {
            //如果没有使用过先初始化在存储
            newValues = AsyncLocalValueMap.Create(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);
        } 
        //给当前线程执行上下文赋值新值
        Thread.CurrentThread._executionContext = (!isFlowSuppressed && AsyncLocalValueMap.IsEmpty(newValues)) ?
            null : new ExecutionContext(newValues, newChangeNotifications, isFlowSuppressed);

    }

我们来看它传递的值newValues,最终也是走进了IAsyncLocalValueMap? m_localValues

看到这里应该能明白,这里赋值到ExecutionContext上下文的IAsyncLocalValueMap? m_localValues中,然后get的时候再查出来

AsyncLocal只是对ExecutionContext进行一层包装,而真正数据流转,统一交给了ExecutionContext操作,至于ExecutionContext的操作,篇幅较多,后续会单独出一期进行深入刨析,大致流程如下:

在这里插入图片描述
当我们切换线程的时候,就会将上下文进行传递出去,最终交给操作系统,当我们切换回来的时候,又会执行回调,将原先的copy的数据进行恢复
在这里插入图片描述
在这里插入图片描述

3、常见问题

我们先抛砖引玉,有这么一种情况,当我们的泛型是一个对象,并在子异步方法里面进行赋值,并不会影响到外层的数据

AsyncLocal<Student> context =new AsyncLocal<Student>();
context.Value = new Student {Name = "张三" };
Console.WriteLine($"Main之前:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() =>
{
    Console.WriteLine($"Task1之前:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine("设置李四");
    context.Value = new Student {Name = "李四" };
    Console.WriteLine($"Task1之后:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"Main之后:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
输出结果:
Main之前:张三,ThreadId=1
Task1之前:张三,ThreadId=6
设置李四
Task1之后:李四,ThreadId=6
Main之后:张三,ThreadId=6

如果我们按照面向过程的思维去考虑,最后一个Main之后输出的竟然不是已经赋值的李四

类似出现了被回档的情况,这个也是非常多刚接触AsyncLocal 容易犯下的错误,那我们来说说,为什么会出现这个问题

我们看看这个:
在这里插入图片描述
赋值的时候,传递的key是一个引用地址

我们想想分析一下,有两个原因:

  1. 查找值的key是一个引用地址
  2. 线程切换传递的是一个浅拷贝对象

如果,我们不更改它的引用,不就可以实现传递了吗?

更改后代码:

AsyncLocal<Student> context =new AsyncLocal<Student>();
context.Value = new Student {Name = "张三" };
Console.WriteLine($"Main之前:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() =>
{
    Console.WriteLine($"Task1之前:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine("设置李四");
    var data= context.Value;
    data.Name="李四";
    Console.WriteLine($"Task1之后:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"Main之后:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");

返回结果:
Main之前:张三,ThreadId=1
Task1之前:张三,ThreadId=6
设置李四
Task1之后:李四,ThreadId=6
Main之后:李四,ThreadId=8

对头,这就是解决方案

4、扩展

如果有看过HttpContextAccessor的源码,肯定对这个很熟悉
因为它也是这么玩的

    private static readonly AsyncLocal<HttpContextAccessor.HttpContextHolder> _httpContextCurrent = new AsyncLocal<HttpContextAccessor.HttpContextHolder>();
    
    public HttpContext? HttpContext
    {
      get => HttpContextAccessor._httpContextCurrent.Value?.Context;
      set
      {
        HttpContextAccessor.HttpContextHolder httpContextHolder = HttpContextAccessor._httpContextCurrent.Value;
        if (httpContextHolder != null)
          httpContextHolder.Context = (HttpContext) null;
        if (value == null)
          return;
        HttpContextAccessor._httpContextCurrent.Value = new HttpContextAccessor.HttpContextHolder()
        {
          Context = value
        };
      }
    }
橙子软件开发C是一种软件开发语言,C语言是一种高级的、面向过程的编程语言,适用于各种不同领域的软件开发。它的特点是简洁、高效、灵活,被广泛应用于嵌入式系统、操作系统、游戏开发等领域。 金橙子软件通过开发C语言,能够实现各种功能。首先,C语言具有出色的性能和速度,使得金橙子软件能够开发出高效的应用程序。其次,C语言提供了丰富的库函数和工具,使得开发过程更加简化和高效。金橙子软件可以利用这些库函数和工具来实现各种功能,如文件操作、内存管理、图形界面等。 由于C语言的广泛应用和大量的学习资料,金橙子软件开发C的成本较低。很多开发者都具备C语言的编程能力,可以快速上手并进行开发。此外,C语言的代码可移植性较高,便于在不同的平台上运行,这为金橙子软件的开发与应用提供了更多的可能性。 金橙子软件开发C的过程可以分为几个步骤。首先,需要明确软件的需求和功能,进行系统设计和架构设计。然后,根据设计进行编码实现,并进行调试和测试。最后,对于开发过程中出现的问题和bug进行修复和优化,完成软件的发布和部署。 总结来说,金橙子软件开发C是一种高效、灵活的软件开发方式,通过C语言的特性和丰富的库函数和工具,金橙子软件能够实现各种功能,并且具备较低的开发成本和高的代码可移植性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值