>>返回《C# 并发编程》

1. 简介

  • 普通共享变量:

    • 在某个类上用静态属性的方式即可。
  • 多线程共享变量
    • 希望能将这个变量的共享范围缩小到单个线程
    • 无关系的B线程无法访问到A线程的值;

[ThreadStatic]特性、ThreadLocal<T>CallContextAsyncLocal<T> 都具备这个特性。

例子:

由于 .NET Core 不再实现 CallContext,所以下列代码只能在 .NET Framework 中执行

class Program
{
//对照
private static string _normalStatic; [ThreadStatic]
private static string _threadStatic;
private static ThreadLocal<string> _threadLocal = new ThreadLocal<string>();
private static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
static void Main(string[] args)
{
Parallel.For(0, 4, _ =>
{
var threadId = Thread.CurrentThread.ManagedThreadId; var value = $"这是来自线程{threadId}的数据"; _normalStatic = value;
_threadStatic = value;
CallContext.SetData("value", value);
_threadLocal.Value = value;
_asyncLocal.Value = value; Console.WriteLine($"Use Normal; Thread:{threadId}; Value:{_normalStatic}");
Console.WriteLine($"Use ThreadStaticAttribute; Thread:{threadId}; Value:{_threadStatic}");
Console.WriteLine($"Use CallContext; Thread:{threadId}; Value:{CallContext.GetData("value")}");
Console.WriteLine($"Use ThreadLocal; Thread:{threadId}; Value:{_threadLocal.Value}");
Console.WriteLine($"Use AsyncLocal; Thread:{threadId}; Value:{_asyncLocal.Value}");
}); Console.Read();
}
}

输出:

Use Normal;                Thread:15; Value:10
Use [ThreadStatic]; Thread:15; Value:15
Use Normal; Thread:10; Value:10
Use Normal; Thread:8; Value:10
Use [ThreadStatic]; Thread:8; Value:8
Use CallContext; Thread:8; Value:8
Use [ThreadStatic]; Thread:10; Value:10
Use CallContext; Thread:10; Value:10
Use CallContext; Thread:15; Value:15
Use ThreadLocal; Thread:15; Value:15
Use ThreadLocal; Thread:8; Value:8
Use AsyncLocal; Thread:8; Value:8
Use ThreadLocal; Thread:10; Value:10
Use AsyncLocal; Thread:10; Value:10
Use AsyncLocal; Thread:15; Value:15

结论:

  • Normal 为对照组
  • Nomal 的 Thread 与 Value 值不同,因为读到了其他线程修改的值
  • 其他的类型,存储的值,在 Parallel 启动的线程间是隔离的

2. 异步下的共享变量

日常开发过程中,我们经常遇到异步的场景。

异步可能会导致代码执行线程的切换。

例如:

测试:[ThreadStatic]特性、ThreadLocal<T>AsyncLocal<T> ,三种共享变量被异步代码赋值后的表现。

class Program
{
[ThreadStatic]
private static string _threadStatic;
private static ThreadLocal<string> _threadLocal = new ThreadLocal<string>();
private static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
static void Main(string[] args)
{
_threadStatic = "set";
_threadLocal.Value = "set";
_asyncLocal.Value = "set";
PrintValuesInAnotherThread();
Console.ReadKey();
} private static void PrintValuesInAnotherThread()
{
Task.Run(() =>
{
Console.WriteLine($"ThreadStatic: {_threadStatic}");
Console.WriteLine($"ThreadLocal: {_threadLocal.Value}");
Console.WriteLine($"AsyncLocal: {_asyncLocal.Value}");
});
}
}

输出:

ThreadStatic:
ThreadLocal:
AsyncLocal: set

结论:

在异步发生后,线程被切换,只有 AsyncLocal 还能够保留原来的值.

  • CallContext 也可以实现这个需求,但 .Net Core 没有被实现,这里就不过多说明。

我们总结一下这些变量的表现:

实现方式 DotNetFx DotNetCore 是否支持数据向辅助线程的
[ThreadStatic]
ThreadLocal
CallContext.SetData(string name, object data) 仅当参数 data 对应的类型实现了 ILogicalThreadAffinative 接口时支持
CallContext.LogicalSetData(string name, object data)
AsyncLocal

辅助线程: 用于处理后台任务,用户不必等待就可以继续使用应用程序,比如线程池线程。

注意:

  • [ThreadStatic]特性、ThreadLocal<T> 最好不要用在线程池线程

    • 线程池线程是可重用的,线程不会销毁,当线程被重用时,之前使用保存的值依然存在,可能造成影响
  • 使用 AsyncLocal<T> 可以用在线程池线程
    • 线程使用后回归线程池, AsyncLocal<T> 的状态会被清除,无法访问之前的值
  • new Task(...) 默认不是新建一个线程,而是使用线程池线程

3. 解析 AsyncLocal

  • AsyncLocal<T>Value 属性的真正的数据存取是通过 ExecutionContextinternal 的方法 GetLocalValueSetLocalValue 将数据存到 当前ExecutionContext 上的 m_localValues 字段上

    • ExecutionContext 会根据执行环境进行流动,详见 《ExecutionContext(执行上下文)综述》
    • 简单描述就是,线程发生切换的时候, ExecutionContext 会在前一个线程中被捕获,流向下一个线程,它所保存的数据也就随之流动了
      • 在所有会发生线程切换的地方,基础类库(BCL) 都为我们封装好了对 ExecutionContext 的捕获
      • 例如:
        • new Thread(...).Start()
        • new Task(...).Start()
        • Task.Run(...)
        • ThreadPool.QueueUserWorkItem(...)
        • await 语法糖
  • m_localValues 类型是 IAsyncLocalValueMap

3.1. IAsyncLocalValueMap 的实现

以下为基础设施提供的实现:

类型 元素个数
EmptyAsyncLocalValueMap 0
OneElementAsyncLocalValueMap 1
TwoElementAsyncLocalValueMap 2
ThreeElementAsyncLocalValueMap 3
MultiElementAsyncLocalValueMap 4 ~ 16
ManyElementAsyncLocalValueMap > 16

随着 ExecutionContext 所关联的 AsyncLocal 数量的增加, IAsyncLocalValueMap实现将会在 ExecutionContextSetLocalValue 方法中被不断替换

  • 查询的时间复杂度和空间复杂度依次递增

3.2. 结论

  • AsyncLocal 类型存储数据,是在自己线程的 ExecutionContext
  • ExecutionContext 的实例会随着异步或者多线程的启动而被流向执行后续代码的其他线程,保证了启动异步的线程存储的数据可以被访问到
  • 数据存到 IAsyncLocalValueMap 类型的变量中,此变量会根据存储的 AsyncLocal 变量个数而切换实现
    • 支持存储量越大的实现类型,性能越

参考资料

《浅析 .NET 中 AsyncLocal 的实现原理》 --- 黑洞视界

多线程共享变量和 AsyncLocal的更多相关文章

  1. 『Python』 多线程 共享变量的实现

    简介: 对于Python2而言,对于一个全局变量,你的函数里如果只使用到了它的值,而没有对其赋值(指a = XXX这种写法)的话,就不需要声明global. 相反,如果你对其赋了值的话,那么你就需要声 ...

  2. Java多线程共享变量控制

    1. 可见性 如果一个线程对共享变量值的修改,能够及时的被其他线程看到,叫做共享变量的可见性.如果一个变量同时在多个线程的工作内存中存在副本,那么这个变量就叫共享变量 2. JMM(java内存模型) ...

  3. EntityFrameworkCore之工作单元的封装

    1. 简介 2. DbContext 生命周期和使用规范 2.1. 生命周期 2.2. 使用规范 2.3. 避免 DbContext 线程处理问题 3. 封装-工作单元 3.1. 分析 3.2. 设计 ...

  4. 谈谈.NET Core下如何利用 AsyncLocal 实现共享变量

    前言 在Web 应用程序中,我们经常会遇到这样的场景,如用户信息,租户信息本次的请求过程中都是固定的,我们希望是这种信息在本次请求内,一次赋值,到处使用.本文就来探讨一下,如何在.NET Core 下 ...

  5. 【.NET深呼吸】基于异步上下文的本地变量(AsyncLocal)

    在开始吹牛之前,老周说两个故事. 第一个故事是关于最近某些别有用心的人攻击.net的事,其实我们不用管它们,只要咱们知道自己是.net爱好者就行了,咱们就是因为热爱.net才会选择它.这些人在这段时间 ...

  6. Win32多线程编程(3) — 线程同步与通信

      一.线程间数据通信 系统从进程的地址空间中分配内存给线程栈使用.新线程与创建它的线程在相同的进程上下文中运行.因此,新线程可以访问进程内核对象的所有句柄.进程中的所有内存以及同一个进程中其他所有线 ...

  7. 04747_Java语言程序设计(一)_第8章_多线程

    例8.1应用程序用Thread子类实现多线程. import java.util.Date; public class Example8_1 { static Athread threadA; sta ...

  8. Java多线程编程模式实战指南(二):Immutable Object模式

    多线程共享变量的情况下,为了保证数据一致性,往往需要对这些变量的访问进行加锁.而锁本身又会带来一些问题和开销.Immutable Object模式使得我们可以在不使用锁的情况下,既保证共享变量访问的线 ...

  9. Python 多线程、多进程 (一)之 源码执行流程、GIL

    Python 多线程.多进程 (一)之 源码执行流程.GIL Python 多线程.多进程 (二)之 多线程.同步.通信 Python 多线程.多进程 (三)之 线程进程对比.多线程 一.python ...

随机推荐

  1. Java入门 - 面向对象 - 05.封装

    原文地址:http://www.work100.net/training/java-encapsulation.html 更多教程:光束云 - 免费课程 封装 序号 文内章节 视频 1 概述 2 封装 ...

  2. GStreamer基础教程13 - 调试Pipeline

    摘要 在很多情况下,我们需要对GStreamer创建的Pipeline进行调试,来了解其运行机制以解决所遇到的问题.为此,GStreamer提供了相应的调试机制,方便我们快速定位问题. 查看调试日志 ...

  3. maven常用的远程仓库地址

    <mirror> <id>nexus-aliyun</id> <name>Nexus aliyun</name> <url>ht ...

  4. Oracle GoldenGate to Confluent with Kafka Connect

    Confluent is a company founded by the team that built Apache Kafka. It builds a platform around Kafk ...

  5. python类型-序列

    注:本文档主要是学习<Python核心编程(第二版)>时做的资料整理. 1.序列 序列的成员是有序排列的,并且可以通过下标偏移量访问到它的一个或者几个成员,包括字符串(普通字符串和Unic ...

  6. 半夜了我来发张图 睡觉 ControllerDescriptor 与 ActionDescriptor 之间 的 关系

  7. Xen+OpenQRM快速部署

    一.选择系统平台 a)   Ubuntu-11.10-server-amd64 二.Xen安装 a)   Xen安装 # apt-get -y install xen-hypervisor-4.1-a ...

  8. es6 promise 简单总结

    话不多说,直捣主题. promise用途:异步编程的一种解决方案. 优点:比传统的解决方案——回调函数和事件——更合理和更强大. 三种状态:pending(进行中).fulfilled(已成功)和re ...

  9. CSS-03-组选择器

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  10. Linux环境下的network IO

    同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案是不同的.所以先限定一下本文的上下文. 本文讨论的背景是Linux环境下的network IO. ...