一文读懂 .NET 中的 ThreadLocal 和 AsyncLocal

d650ac342713076d40134e43fc50a3ec.png

e49212c9fc613129b85321532ade7c60.gif

cb3f6e840041f96a6f2beaf0f527bbc9.png

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

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

8aa8819f81470511bbe4d6be94e21caa.jpeg


基本概念

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

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


多线程的数据问题

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

static string value;
static async Task Main(string[] args)
{
    _ = Print("James");
    _ = Print("Bob");
    _ = Print("Tony");

    Console.ReadKey();
}

static async 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 进行赋值,并进行输出,运行结果会怎么样?

862130dd316c7ced3c5fedf7f9bf47c6.png

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


使用 ThreadLocal

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

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

static string value;
static readonly ThreadLocal<string> _threadLocal = new ThreadLocal<string>();

static async Task Main(string[] args)
{
    _ = Print("James");
    _ = Print("Bob");
    _ = Print("Tony");

    Console.ReadKey();
}

static async Task Print(string name)
{
    await Task.Run(() =>
    {
        _threadLocal.Value = name;

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

    });
}

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

aaad7e2a7c36e514c0f42add715e7182.png

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

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

static async 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}");

    });
}

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

954b0a0aca4e187f939870af16f98fd8.png

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

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

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

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



使用 AsyncLocal


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

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

static string value;
static readonly ThreadLocal<string> _threadLocal = new ThreadLocal<string>();
static readonly AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();

static async Task Main(string[] args)
{
    _ = Print("James");
    _ = Print("Bob");
    _ = Print("Tony");

    Console.ReadKey();
}

static async 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}");

    });
}

64a3b888b61f55a645f2ff2e6b35aaff.png

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

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



AsyncLocal 的工作原理

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

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

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

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

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

.NET 异步编程

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

827e1f373043c1c26aecdbbf85a4e84c.png

分享

ebb2ac953095130baa7e0090dc7559c8.png

点收藏 

32f37d8859b2b1ff36fb4cf9ab480c96.png

点点赞

053128c56fd13d877928bae44dc04eae.png

点在看

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值