.NET AsyncLocal 避坑指南(二)


风晓
风晓 2023-12-31 09:24:06 53905 赞同 0 反对 0
分类: 资源
接上一篇.NET AsyncLocal 避坑指南(一)

所以上面代码的执行过程如下:

AsyncLocal 的避坑指南

那么我们如何在 FooAsync 方法中修改 AsyncLocal 的值,并且在 Main 方法中获取到修改后的值呢?

我们需要借助一个中介者,让中介者来保存 AsyncLocal 的值,然后在 FooAsync 方法中修改中介者的属性值,这样就可以在 Main 方法中获取到修改后的值了。

下面我们设计一个 ValueHolder 来保存 AsyncLocal 的值,修改 Value 并不会修改 AsyncLocal 的值,而是修改 ValueHolder 的属性值,这样就不会触发 ExecutionContext 的 COW。

我们还需要设计一个 ValueAccessor 来封装 ValueHolder 对值的访问和修改,这样可以保证 ValueHolder 的值只能在 ValueAccessor 中被修改。

class ValueAccessor<T> : IValueAccessor<T>
{
    private static AsyncLocal<ValueHolder<T>> _asyncLocal = new AsyncLocal<ValueHolder<T>>();

    public T Value
    {
        get => _asyncLocal.Value != null ? _asyncLocal.Value.Value : default;
        set
        {
            _asyncLocal.Value ??= new ValueHolder<T>();

            _asyncLocal.Value.Value = value;
        }
    }
}

class ValueHolder<T>
{
    public T Value { get; set; }
}

class Program
{
    private static IValueAccessor<string> _valueAccessor = new ValueAccessor<string>();

    static async Task Main(string[] args)
    {
        _valueAccessor.Value = "A";
        Console.WriteLine($"ValueAccessor before await FooAsync in Main: {_valueAccessor.Value}");
        await FooAsync();
        Console.WriteLine($"ValueAccessor after await FooAsync in Main: {_valueAccessor.Value}");
    }

    private static async Task FooAsync()
    {
        _valueAccessor.Value = "B";
        Console.WriteLine($"ValueAccessor before await in FooAsync: {_valueAccessor.Value}");
        await Task.Delay(100);
        Console.WriteLine($"ValueAccessor after await in FooAsync: {_valueAccessor.Value}");
    }
}

输出结果:

ValueAccessor before await FooAsync in Main: A
ValueAccessor before await in FooAsync: B
ValueAccessor after await in FooAsync: B
ValueAccessor after await FooAsync in Main: B

HttpContextAccessor 的实现原理

我们常用的 HttpContextAccessor 通过HttpContextHolder 来间接地在 AsyncLocal 中存储 HttpContext。

如果要更新 HttpContext,只需要在 HttpContextHolder 中更新即可。因为 AsyncLocal 的值不会被修改,更新 HttpContext 时 ExecutionContext 也不会出现 COW 的情况。

不过 HttpContextAccessor 中的逻辑有点特殊,它的 HttpContextHolder 是为保证清除 HttpContext 时,这个 HttpContext 能在所有引用它的 ExecutionContext 中被清除(可能因为修改 HttpContextHolder 之外的 AsyncLocal 数据导致 ExecutionContext 已经 COW 很多次了)。

下面是 HttpContextAccessor 的实现,英文注释是原文,中文注释是我自己的理解。

/// </summary>
public class HttpContextAccessor : IHttpContextAccessor
{
    private static readonly AsyncLocal<HttpContextHolder> _httpContextCurrent = new AsyncLocal<HttpContextHolder>();

    /// <inheritdoc/>
    public HttpContext? HttpContext
    {
        get
        {
            return _httpContextCurrent.Value?.Context;
        }
        set
        {
            var holder = _httpContextCurrent.Value;
            if (holder != null)
            {
                // Clear current HttpContext trapped in the AsyncLocals, as its done.
                // 这边的逻辑是为了保证清除 HttpContext 时,这个 HttpContext 能在所有引用它的 ExecutionContext 中被清除
                holder.Context = null;
            }

            if (value != null)
            {
                // Use an object indirection to hold the HttpContext in the AsyncLocal,
                // so it can be cleared in all ExecutionContexts when its cleared.
                // 这边直接修改了 AsyncLocal 的值,所以会导致 ExecutionContext 的 COW。新的 HttpContext 不会被传递到原先的 ExecutionContext 中。
                _httpContextCurrent.Value = new HttpContextHolder { Context = value };
            }
        }
    }

    private sealed class HttpContextHolder
    {
        public HttpContext? Context;
    }
}

但 HttpContextAccessor 的实现并不允许将新赋值的非 null 的 HttpContext 传递到外层的 ExecutionContext 中,可以参考上面的源码及注释理解。

class Program
{
    private static IHttpContextAccessor _httpContextAccessor = new HttpContextAccessor();
    
    static async Task Main(string[] args)
    {
        var httpContext = new DefaultHttpContext
        {
            Items = new Dictionary<object, object>
            {
                { "Name", "A"}
            }
        };
        _httpContextAccessor.HttpContext = httpContext;
        Console.WriteLine($"HttpContext before await FooAsync in Main: {_httpContextAccessor.HttpContext.Items["Name"]}");
        await FooAsync();
        // HttpContext 被清空了,下面这行输出 null
        Console.WriteLine($"HttpContext after await FooAsync in Main: {_httpContextAccessor.HttpContext?.Items["Name"]}");
    }

    private static async Task FooAsync()
    {
        _httpContextAccessor.HttpContext = new DefaultHttpContext
        {
            Items = new Dictionary<object, object>
            {
                { "Name", "B"}
            }
        };
        Console.WriteLine($"HttpContext before await in FooAsync: {_httpContextAccessor.HttpContext.Items["Name"]}");
        await Task.Delay(1000);
        Console.WriteLine($"HttpContext after await in FooAsync: {_httpContextAccessor.HttpContext.Items["Name"]}");
    }
}

输出结果:

HttpContext before await FooAsync in Main: A
HttpContext before await in FooAsync: B
HttpContext after await in FooAsync: B
HttpContext after await FooAsync in Main: 

如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!

评价 0 条
风晓L1
粉丝 1 资源 2038 + 关注 私信
最近热门资源
桌面通用(全架构)【在双系统环境下隐藏Windows启动菜单】操作指南  2112
银河麒麟桌面操作系统V10(SP1)2203-如何进行远程桌面互访?  2022
银河麒麟桌面操作系统【保留数据盘重装系统】  1833
麒麟系统各种原因开不了机解决(合集)  1648
桌面通用(全架构)【rpm包转成deb包】操作方法  933
银河麒麟桌面操作系统 V10-SP1 双系统安装 efi 分区问题  919
统信系统安装(合集)  870
统信桌面专业版【手动分区安装UOS系统】介绍  852
统启动异常几种类型(initramfs 模式)  692
银河麒麟服务器操作系统V10(X86|ARM)【进入单用户模式】操作方法  24
最近下载排行榜
桌面通用(全架构)【在双系统环境下隐藏Windows启动菜单】操作指南 0
银河麒麟桌面操作系统V10(SP1)2203-如何进行远程桌面互访? 0
银河麒麟桌面操作系统【保留数据盘重装系统】 0
麒麟系统各种原因开不了机解决(合集) 0
桌面通用(全架构)【rpm包转成deb包】操作方法 0
银河麒麟桌面操作系统 V10-SP1 双系统安装 efi 分区问题 0
统信系统安装(合集) 0
统信桌面专业版【手动分区安装UOS系统】介绍 0
统启动异常几种类型(initramfs 模式) 0
银河麒麟服务器操作系统V10(X86|ARM)【进入单用户模式】操作方法 0
作者收入月榜
1

prtyaa 收益393.72元

2

zlj141319 收益221.42元

3

1843880570 收益214.2元

4

IT-feng 收益213.03元

5

风晓 收益208.24元

6

777 收益172.82元

7

Fhawking 收益106.6元

8

信创来了 收益105.89元

9

克里斯蒂亚诺诺 收益91.08元

10

技术-小陈 收益79.65元

请使用微信扫码

加入交流群

请使用微信扫一扫!