c#: AsyncLocal的使用

环境:

  • window11
  • vs2022
  • .net core 3.1
  • .net 6.0

一、先说ThreadLocal

在以前写代码的时候,还没有异步的概念,那个时候我们处理HTTP请求就只用一个线程就搞定了,有的时候我们想在代码中共享一个对象,我们希望将这个对象绑定到线程上。
如下:

class Program
{
    private static ThreadLocal<WebContext> threadLocal = new ThreadLocal<WebContext>(() =>
    {
        var ctx = new WebContext();
        Console.WriteLine($"创建WebContext");
        return ctx;
    });
    static void Main(string[] args)
    {
        Console.WriteLine($"主线程: {Thread.CurrentThread.ManagedThreadId}");
        //模拟5个HTTP请求
        for (var i = 0; i < 5; i++)
        {
            var index = i;
            Task.Factory.StartNew(() =>
            {
                var ctx = threadLocal.Value;
                ctx.Name = "请求" + index;
                ctx.Id = index;
                Console.WriteLine($"请求结束:{index} ctx.Name={ctx.Name} ctx.Id={ctx.Id}");
            });
        }
        Console.Read();
    }
}

class WebContext
{
    public string Name { get; set; }
    public int Id { get; set; }
}

c#: AsyncLocal的使用_第1张图片

可以看到,ThreadLocal就像是一个容器,它盛放的东西可以自动区分不同的线程。

二、认识AsyncLocal

在理解了ThreadLocal的作用后,我们就能很好的理解AsyncLocal了。
因为,现在的程序为了应对高并发、提高程序性能,不再是一个线程专门处理一个HTTP请求从接收到返回,而是在多个线程间切换,总之,使用了异步的代码在处理一个HTTP请求的时候可能会有多个线程参与。
所以,AsyncLocal是为了在异步代码间共享对象的。
如下代码:

class Program
{
    private static AsyncLocal<WebContext> asyncLocal = new AsyncLocal<WebContext>();
    static void Main(string[] args)
    {
        Console.WriteLine($"主线程: {Thread.CurrentThread.ManagedThreadId}");
        //模拟5个HTTP请求
        for (var i = 0; i < 5; i++)
        {
            var index = i;
            Task.Factory.StartNew(async () =>
            {
                await ProcessRequest(index);
            });
        }
        Console.Read();
    }

    private static async Task ProcessRequest(int i)
    {
        var ctx = new WebContext()
        {
            Name = "请求" + i,
            Id = i,
        };
        asyncLocal.Value = ctx;
        await InnerProcess(i);
        Console.WriteLine($"请求 {i} end ctx.Name={ctx.Name} ctx.Id={ctx.Id}");

    }

    private static async Task InnerProcess(int i)
    {
        Thread.Sleep(100);
        var ctx = asyncLocal.Value;
        Console.WriteLine($"请求 {i} ctx.Name={ctx.Name} ctx.Id={ctx.Id}");
        ctx.Name += ctx.Id;
    }
}

class WebContext
{
    public string Name { get; set; }
    public int Id { get; set; }
}

c#: AsyncLocal的使用_第2张图片
可以看到,使用AsyncLocal可以让WebContext在不同的异步上下文环境中共享和隔离。

注意,这里说的异步上下文是具有async声明的方法,而我们常见的代码同时具有async声明和内部await代码调用。

为了探索AsyncLocal在不同异步上下文环境中的表现,看下面的代码:

class Program
{
    private static AsyncLocal<WebContext> asyncLocal = new AsyncLocal<WebContext>();
    static async Task Main(string[] args)
    {
        await Async();
        Console.Read();
    }

    //父上下文
    public static async Task Async()
    {
        asyncLocal.Value = new WebContext
        {
            Id = 0,
            Name = "父"
        };
        Console.WriteLine("父:设定ctx:" + asyncLocal.Value);
        await Async1();
        Console.WriteLine("父:结束时ctx:" + asyncLocal.Value);

    }

    //子上下文
    public static async Task Async1()
    {
        Console.WriteLine("    子读取到ctx:" + asyncLocal.Value);
        await Async1_1();
        Console.WriteLine("    经过孙处理后再读取ctx:" + asyncLocal.Value);
        asyncLocal.Value = new WebContext
        {
            Name = "子",
            Id = 1,
        };
        Console.WriteLine("    子改动ctx为:" + asyncLocal.Value);
        await Async1_1();
        Console.WriteLine("    经过孙处理后再读取ctx:" + asyncLocal.Value);
    }

    //孙上下文
    public static async Task Async1_1()
    {
        Console.WriteLine("        孙读取到ctx:" + asyncLocal.Value);
        asyncLocal.Value = new WebContext
        {
            Name = "孙",
            Id = 2,
        };
        Console.WriteLine("        孙改动ctx为:" + asyncLocal.Value);
    }
}

class WebContext
{
    public string Name { get; set; }
    public int Id { get; set; }

    public override string ToString()
    {
        return $"Name={Name},Id={Id}";
    }
}

用图表现上面的形式如下:
c#: AsyncLocal的使用_第3张图片

运行结果如下:
c#: AsyncLocal的使用_第4张图片

由此可见,AsyncLocal中的对象在异步上线文中是自顶向下传递的,父上下文设定的值,子对象可以读取到,同时子上下文也可以进行更改,但其更改只能影响到孙、重孙等上下文而不能影响父上下文。

三、AsyncLocal使用注意事项

  • AsyncLocal主要是用来在同一个异步控制流内共享对象的,如:一个web请求经过多个 async/await 方法调用后(可能切换了多个线程)依然可以共享同一个对象;

  • AsyncLocal存在层级嵌套的特点,不像ThreadLocal一个线程到底,也就是说AsyncLocal是工作在树形的异步控制流上的;

  • AsyncLocal在树形异步控制流上流动的特点:

    • 每个节点都可以有自己的对象;
    • 当子节点没有设置对象时,则访问的是父节点的对象;
    • 当子节点设置了对象时,则访问自己设置的对象;
    • 父节点无法访问子节点设置的对象;
  • AsyncLocal的新节点触发一般发生在async/awaitTask.Run()等代码上;

  • 如果我们的代码既没有 async/await 也没有 类似Task.Run(),那么AsyncLocal是不会触发新节点生成的;

四、AsyncLocal应用实例

考虑到有这样一个需求:

有一个数据库访问对象(DBAccess db=null),我们想实现如下方法:

public abstract class DBAccess
{
	//执行sql语句,如果已经开启的长链接或事务就使用当前的链接或事务
	public async Task ExecuteAsync(string sql);
	
    //开启一个长链接
    //效果就是: using(var conn = new SqlConnection(str)){ /* func代码将在这个conn上工作 */}
	public async Task RunInSessionAsync(Func<Task> func);

    //开启一个事务,func里面的代码都将在这个事务下运行
	public async Task RunInTransaction(Func<Task> func);

	//清空当前的会话和事务,func里面的代码将在新的链接下运行
	public async Task RunInNoSessionAsync(Func<Task> func);
}

这就要求,我们要手动控制每个scope上下文的生成,而不是依赖于async/await的自动,为此封装了下面的类:

/// 
/// 基于  实现的手动控制异步上下文
/// 在 异步控制流 上每个 async/await 或 Task.Run() 方法的调用都会产生新的 一个异步代码块, 这些异步代码块形成了一个树状结构. 这个树状结构有以下特点:
/// /// 每个节点可以有一个对象(注意: 值类型和引用类型的处理方式本质是相同的); /// 子节点可以读取父节点设置的对象,但是不可以改写; /// 子节点也可以设置一个对象,设置后,子节点及其孙节点都可以读取到这个对象; /// 父节点读取不到子节点设置的对象; /// /// 基于上面的特性,ScopeContext提供了通过方法包裹达到手动控制 异步代码块 对象的目的.

/// 使用示例(以异步代码块为例,同步是一样的效果):
/// /// public static async Task Main(string[] args) /// { /// var ctx = ScopeContext.Current; /// ctx.SetProperty("name", "outer"); /// Console.WriteLine($"outer:{ctx}"); /// await ScopeContext.RunInScopeAsync(async () => /// { /// ctx = ScopeContext.Current; /// ctx.SetProperty("name", "middle"); /// Console.WriteLine($" middle:{ctx}"); /// await ScopeContext.RunInScopeAsync(async () => /// { /// ctx = ScopeContext.Current; /// ctx.SetProperty("name", "inner"); /// Console.WriteLine($" inner:{ctx}"); /// await Task.CompletedTask; /// }); /// ctx = ScopeContext.Current; /// Console.WriteLine($" middle:{ctx}"); /// }); /// ctx = ScopeContext.Current; /// Console.WriteLine($"outer:{ctx}"); /// /// Console.WriteLine("ok"); /// Console.ReadLine(); /// } /// //输出: /// //outer:{"Id":1,"Dic":{"name":"outer"}} /// // middle: { "Id":2,"Dic":{ "name":"middle"} } /// // inner: { "Id":3,"Dic":{ "name":"inner"} } /// // middle: { "Id":2,"Dic":{ "name":"middle"} } /// //outer: { "Id":1,"Dic":{ "name":"outer"} } /// ///
public class ScopeContext { public static int _count = 0; public int _id = 0; /// /// 递增Id /// public int Id => _id; private ConcurrentDictionary<object, object> _dic = new ConcurrentDictionary<object, object>(); private static AsyncLocal<ScopeContext> _scopeContext = new AsyncLocal<Sc opeContext>() { Value = new ScopeContext() }; /// /// 当前异步控制流节点的上下文 /// public static ScopeContext Current => _scopeContext.Value; public ScopeContext() { _id = Interlocked.Increment(ref _count); } /// /// 便于调试 /// /// public override string ToString() { return new { Id = Id, Dic = _dic }.ToJson(); } /// /// 在当前异步控制流节点上存数据 /// /// /// public void SetProperty(object key, object value) { _dic.TryAdd(key, value); } /// /// 在当前异步控制节点上取数据 /// /// /// public object GetProperty(object key) { _dic.TryGetValue(key, out object value); return value; } /// /// 开启一个新的异步控制流节点(生成一个新的上下文对象) /// /// public static void RunInScope(Action func) { RunInScope(() => { func(); return 1; }); } /// /// 开启一个新的异步控制流节点(生成一个新的上下文对象) /// /// /// /// public static T RunInScope<T>(Func<T> func) { return Task.Run(() => { _scopeContext.Value = new ScopeContext(); return func(); }).Result; } /// /// 开启一个新的异步控制流节点(生成一个新的上下文对象) /// /// /// public static async Task RunInScopeAsync(Func<Task> func) { await RunInScopeAsync(async () => { await func(); return 1; }); } /// /// 开启一个新的异步控制流节点(生成一个新的上下文对象) /// /// /// /// public static async Task<T> RunInScopeAsync<T>(Func<Task<T>> func) { _scopeContext.Value = new ScopeContext(); return await func(); } }

测试代码如下:

public class TestScope
{
    //同步测试
    public static void Main(string[] args)
    {
        var ctx = ScopeContext.Current;
        ctx.SetProperty("name", "outer");
        Console.WriteLine($"outer:{ctx}");
        ScopeContext.RunInScope(() =>
        {
            ctx = ScopeContext.Current;
            ctx.SetProperty("name", "middle");
            Console.WriteLine($"    middle:{ctx}");
            ScopeContext.RunInScope(() =>
            {
                ctx = ScopeContext.Current;
                ctx.SetProperty("name", "inner");
                Console.WriteLine($"        inner:{ctx}");
            });
            ctx = ScopeContext.Current;
            Console.WriteLine($"    middle:{ctx}");
        });
        ctx = ScopeContext.Current;
        Console.WriteLine($"outer:{ctx}");

        Console.WriteLine("ok");
        Console.ReadLine();
    }

    异步测试
    //public static async Task Main(string[] args)
    //{
    //    var ctx = ScopeContext.Current;
    //    ctx.SetProperty("name", "outer");
    //    Console.WriteLine($"outer:{ctx}");
    //    await ScopeContext.RunInScopeAsync(async () =>
    //    {
    //        ctx = ScopeContext.Current;
    //        ctx.SetProperty("name", "middle");
    //        Console.WriteLine($"    middle:{ctx}");
    //        await ScopeContext.RunInScopeAsync(async () =>
    //        {
    //            ctx = ScopeContext.Current;
    //            ctx.SetProperty("name", "inner");
    //            Console.WriteLine($"        inner:{ctx}");
    //            await Task.CompletedTask;
    //        });
    //        ctx = ScopeContext.Current;
    //        Console.WriteLine($"    middle:{ctx}");
    //    });
    //    ctx = ScopeContext.Current;
    //    Console.WriteLine($"outer:{ctx}");

    //    Console.WriteLine("ok");
    //    Console.ReadLine();
    //}
}

同步和异步测试均如下:
c#: AsyncLocal的使用_第5张图片

你可能感兴趣的:(c#,.net,AsyncLocal)