环境:
- 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; }
}
可以看到,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; }
}
可以看到,使用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}";
}
}
用图表现上面的形式如下:
运行结果如下:
由此可见,AsyncLocal中的对象在异步上线文中是自顶向下传递的,父上下文设定的值,子对象可以读取到,同时子上下文也可以进行更改,但其更改只能影响到孙、重孙等上下文而不能影响父上下文。
三、AsyncLocal使用注意事项
-
AsyncLocal主要是用来在同一个
异步控制流
内共享对象的,如:一个web请求经过多个 async/await 方法调用后(可能切换了多个线程)依然可以共享同一个对象; -
AsyncLocal存在层级嵌套的特点,不像ThreadLocal一个线程到底,也就是说AsyncLocal是工作在树形的
异步控制流
上的; -
AsyncLocal在树形
异步控制流
上流动的特点:- 每个节点都可以有自己的对象;
- 当子节点没有设置对象时,则访问的是父节点的对象;
- 当子节点设置了对象时,则访问自己设置的对象;
- 父节点无法访问子节点设置的对象;
-
AsyncLocal的新节点触发一般发生在
async/await
或Task.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的自动,为此封装了下面的类:
/// <summary>
/// 基于 <seealso cref="AsyncLocal{T}"/> 实现的手动控制异步上下文<br />
/// 在 异步控制流 上每个 async/await 或 Task.Run() 方法的调用都会产生新的 一个异步代码块, 这些异步代码块形成了一个树状结构. 这个树状结构有以下特点:<br />
/// <list type="bullet">
/// <item>每个节点可以有一个对象(注意: 值类型和引用类型的处理方式本质是相同的);</item>
/// <item>子节点可以读取父节点设置的对象,但是不可以改写;</item>
/// <item>子节点也可以设置一个对象,设置后,子节点及其孙节点都可以读取到这个对象;</item>
/// <item>父节点读取不到子节点设置的对象;</item>
/// </list>
/// 基于上面的特性,ScopeContext提供了通过方法包裹达到手动控制 异步代码块 对象的目的.<br /><br />
/// 使用示例(以异步代码块为例,同步是一样的效果): <br />
/// <code>
/// 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"} }
/// </code>
/// </summary>
public class ScopeContext
{
public static int _count = 0;
public int _id = 0;
/// <summary>
/// 递增Id
/// </summary>
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() };
/// <summary>
/// 当前异步控制流节点的上下文
/// </summary>
public static ScopeContext Current => _scopeContext.Value;
public ScopeContext()
{
_id = Interlocked.Increment(ref _count);
}
/// <summary>
/// 便于调试
/// </summary>
/// <returns></returns>
public override string ToString()
{
return new
{
Id = Id,
Dic = _dic
}.ToJson();
}
/// <summary>
/// 在当前异步控制流节点上存数据
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void SetProperty(object key, object value)
{
_dic.TryAdd(key, value);
}
/// <summary>
/// 在当前异步控制节点上取数据
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public object GetProperty(object key)
{
_dic.TryGetValue(key, out object value);
return value;
}
/// <summary>
/// 开启一个新的异步控制流节点(生成一个新的上下文对象)
/// </summary>
/// <param name="func"></param>
public static void RunInScope(Action func)
{
RunInScope(() =>
{
func();
return 1;
});
}
/// <summary>
/// 开启一个新的异步控制流节点(生成一个新的上下文对象)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="func"></param>
/// <returns></returns>
public static T RunInScope<T>(Func<T> func)
{
return Task.Run(() =>
{
_scopeContext.Value = new ScopeContext();
return func();
}).Result;
}
/// <summary>
/// 开启一个新的异步控制流节点(生成一个新的上下文对象)
/// </summary>
/// <param name="func"></param>
/// <returns></returns>
public static async Task RunInScopeAsync(Func<Task> func)
{
await RunInScopeAsync(async () =>
{
await func();
return 1;
});
}
/// <summary>
/// 开启一个新的异步控制流节点(生成一个新的上下文对象)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="func"></param>
/// <returns></returns>
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();
//}
}
同步和异步测试均如下: