当前位置: 欣欣网 > 码农

一文读懂 .NET 中的 ThreadLocal 和 AsyncLocal

2024-04-18码农

欢迎来到 Dotnet 工具箱 !在这里,你可以发现各种令人惊喜的开源项目!

本篇文章,我们主要介绍一下 C# 中关于异步操作的一个重要知识点,ThreadLocal 和 AsyncLocal。


基本概念

ThreadLocal 用于在多线程环境中创建线程局部变量,可以让每个线程独立地访问自己的变量副本,互不影响。

而 AsyncLocal 是 ThreadLocal 的异步版本,专门用于异步编程场景,在异步操作中它可以正确处理上下文切换。


多线程的数据问题

接下来,我们通过一个例子,看一下它们是如何使用的?以及它们之间的区别。

staticstringvalue;
staticasync Task Main(string[] args)
{
_ = Print("James");
_ = Print("Bob");
_ = Print("Tony");
Console.ReadKey();
}
staticasync Task Print(string name)
{
await Task.Run(() =>

value = name;
Console.WriteLine($"参数: {name} 当前线程: {Thread.CurrentThread.ManagedThreadId} 当前值:{value}");
});
}

在上面的代码中,定义了 静态共享变量 value 值,然后定义一个 Print 打印方法,传入参数 name,使用 Task.Run 开启新的线程,然后把 静态变量 value 设置为 name,并使用 Console.Writeline 输出。

最后,我们执行 3 次 Print 方法,并传入不同的 name,这样,3个线程同时对共享变量 value 进行赋值,并进行输出,运行结果会怎么样?

程序运行结果,前面是传入的参数分别是 3个 name,线程 id 也不一样,而共享变量的值,已经出现了混乱(全部为 Tony),在多线程环境中,就会出现这样的情况。

使用 ThreadLocal

这种情况,我们就可以使用 ThreadLocal 来解决,它可以保证每个线程的数据都是独立的,互不影响。

定义一个静态的 ThreadLocal,然后修改 Print 方法,把 value 换成 threadLocal 的 Value 值,输出的值也修改为 threadLocal.Value.

staticstringvalue;
staticreadonly ThreadLocal<string> _threadLocal = new ThreadLocal<string>();
staticasync Task Main(string[] args)
{
_ = Print("James");
_ = Print("Bob");
_ = Print("Tony");
Console.ReadKey();
}
staticasync Task Print(string name)
{
await Task.Run(() =>
{
_threadLocal.Value = name;
Console.WriteLine($"参数: {name} 当前线程: {Thread.CurrentThread.ManagedThreadId} 当前值:{_threadLocal.Value}");
});
}



再次运行程序查看输出结果

可以看到,现在每个线程的数据现在都是正确的了,并没有受到其他线程的影响,这就是 ThreadLocal 的功能作用。

接下来,我们修改 Print 方法,使用 Task.Delay 等待 1秒,然后再一次输出, 如下

staticasync Task Print(string name)
{
await Task.Run(async () =>

_threadLocal.Value = name;
Console.WriteLine($"参数: {name} 当前线程: {Thread.CurrentThread.ManagedThreadId} 当前值:{_threadLocal.Value}");
await Task.Delay(1000);
Console.WriteLine($"参数: {name} 当前线程: {Thread.CurrentThread.ManagedThreadId} 当前值:{_threadLocal.Value}");
});
}


那么两次输出的结果会一样吗?

可以看到,第一次输出没有问题,而第二次输出,数据已经不正确了。

这是因为我们使用 Task 的 await 以后,线程就有可能发生切换,当然这是后台的线程池来分配的。

线程发生改变以后,这里 ThreadLocal 就不能满足我们的需求了。

所以,针对这种不是同一个线程的异步场景,我们可以使用 AsyncLocal,它是 ThreadLocal的异步替代版本。

使用 AsyncLocal

使用也非常简单,只需要把 ThreadLocal 改成 AsyncLocal 即可。

然后我们修改 Print 方法,同样修改为 AsyncLocal,如下

staticstringvalue;
staticreadonly ThreadLocal<string> _threadLocal = new ThreadLocal<string>();
staticreadonly AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
staticasync Task Main(string[] args)
{
_ = Print("James");
_ = Print("Bob");
_ = Print("Tony");
Console.ReadKey();
}
staticasync Task Print(string name)
{
await Task.Run(async () =>
{
_asyncLocal.Value = name;
Console.WriteLine($" A {name} 当前线程: {Thread.CurrentThread.ManagedThreadId} 当前值:{_asyncLocal.Value}");
await Task.Delay(1000);
Console.WriteLine($" B {name} 当前线程: {Thread.CurrentThread.ManagedThreadId} 当前值:{_asyncLocal.Value}");
});
}






可以看到,就算线程发生了改变,两次输出的结果都是一样的,使用 AsyncLocal 解决了问题。

所以,在异步编程中,推荐使用 AsyncLocal,除非你能保证它一直使用的是同一个线程,那么可以使用 ThreadLocal。

AsyncLocal 的工作原理

为什么 AsyncLocal 在不同的线程中,也能访问到相同的数据呢?

这里就要提到执行上下文 (ExecutionContext), 我们知道,使用 Task 以后,线程池会在后台自动分配空闲的线程来执行。

当程序创建 Task 的时候,它会拿到上一个 Task 的执行上下文,然后保存在当前 Task 的内部变量中。

在新的线程上执行当前这个 Task 的时候,首先会在线程上恢复执行上下文,再执行 Task。

所以,Task 在哪个线程是执行是不重要的,因为执行上下文是一直流动传递的,所以可以访问到一致的数据。



.NET 异步编程

当你渴望成为.NET架构师时,深入理解异步编程是至关重要的。如果你想更深入地了解相关内容,可以关注下方提供的.NET架构师课程,希望这对你有所帮助~

分享

点收藏

点点赞

点在看