【C# 线程】线程局部存储(TLS) 实战部分 ThreadStatic|LocalDataStoreSlot|ThreadLocal<T>
往袋子里面装苹果 错误案例示范
关于C#多线程的文章,大部分都在讨论线程的起停或者是多线程同步问题。多线程同步就是在不同线程中访问同一个变量(一般是线程工作函数外部的变量),众所周知在不使用线程同步的机制下,由于竟态的存在会使某些线程产生脏读或者是覆盖其它线程已写入的值(各种混乱)。而另外一种情况就是我们想让线程所访问的变量属于线程自身所有,这就是所谓的线程本地变量。
下文我们将逐渐扩展一个最简单的示例代码,来展示上面所说的变量并发访问以及线程本地变量的区别和各自解决方案。
这里要展示的例子很简单。所访问的变量是一个“袋子内苹果的数量”,而工作函数就是“往袋子里放苹果”。
public class Bag
{
public int AppleNum { get; set; }
} public class Test
{
public void TryTwoThread()
{
var b = new Bag();
Action localAct = () =>
{
for (int i = 0; i < 10; i++)
{
++b.AppleNum;
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {b.AppleNum}");
Thread.Sleep(100);
}
};
Parallel.Invoke(localAct, localAct);
}
} // Program.cs
var tester = new Test();
tester.TryTwoThread();
如代码所示,这是一段经典的多线程变量并发访问错误的代码。由于没有任何并发访问控制的代码,所以执行结果是不确定的。我们期望的结果是有20个苹果在袋子种,实际情况下很难达到这个结果。

往袋子里面装苹果 正确案例示范
由于执行结果不确定,所以上面只是展示了其中一种随机出现的情况。
解决这个问题的方法就是使用并发控制,最容易的方法就是给共享变量的访问加个锁。
public class Test
{
private object _locker = new object(); public void TryTwoThread()
{
var b = new Bag();
Action localAct = () =>
{
for (int i = 0; i < 10; i++)
{
lock(_locker)
{
++b.AppleNum;
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {b.AppleNum}");
}
Thread.Sleep(100);
}
};
Parallel.Invoke(localAct, localAct);
}
}
这样执行结果就能得到保障,最终袋子里就会有20个苹果。当然还有其它并发控制方法,但那不是本文重点忽略不说。

往袋子里面装苹果 案例示范1
在某些场景下我们会有另一种需求,我们关心的是每个线程往袋子里放了多少个苹果。这时我们就需要让Bag对象与线程相关(有多个袋子,每个袋子为线程所有)。这就需要用到本文重点要介绍的内容 - 线程本地变量。
在不使用线程本地变量的情况下,实现上述目的的一个简单方法是把变量放入工作函数内部,作为函数内部变量。
public class Test
{
public void TryTwoThread()
{
Action localAct = () =>
{
var b = new Bag(); //把变量访问工作函数当中
for (int i = 0; i < 10; i++)
{
++b.AppleNum;
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {b.AppleNum}");
Thread.Sleep(100);
} };
Parallel.Invoke(localAct, localAct);
}
}
可以看到结果如我们所愿。

如果我们的工作函数是独立于一个类中,且要并发的访问的变量是这个类的成员,上面这种方法就不适用了。
前面的例子种的Action换成如下的工作类:
public class Worker
{
private Bag _bag = new Bag(); public void PutTenApple()
{
for (int i = 0; i < 10; i++)
{
PutApple();
Show();
Thread.Sleep(100);
}
} private void PutApple()
{
++_bag.AppleNum;
} private void Show()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {_bag.AppleNum}");
}
}
测试方法改为:
public void TryTwoThread()
{
var worker = new Worker();
Parallel.Invoke(worker.PutTenApple, worker.PutTenApple);
}
注意上面的Worker类也是一个不满足我们每个线程独立操作自己关联变量要求的例子。而且由于没有并发控制,程序的执行结果不可控。
我们也可以将
_bag变量声明于PutTenApple中来实现与线程本地变量一样的效果,但那样在调用PutApple和Show方法时就免不了传参数。
下面开始介绍几种实现线程本地变量的方法。
往袋子里面装苹果 案例示范--线程相关的静态字段
第一种方法线程相关的静态字段是使用ThreadStaticAttribute。这也是微软推荐的性能更好的方法。
其做法是将成员变量声明为static并打上[ThreadStatic]这个标记。我们在之前代码的基础上做如下修改:
[ThreadStatic] private static Bag _bag = new Bag();
注意这个实现是有问题的。下面会详细介绍。
如果你的VS上也安装有Resharper这个宇宙级插件,你会看到在初始化这个静态变量的代码下会有这样的提示:

关于这个提示,ReSharper官网也有解释。
简单来说,就是上面的初始化器只会被调用一次,导致的结果就是只有第一个执行此方法的线程能正确获取到_bag成员的值,之后的进程再访问_bag时,会发现_bag仍是未初始化状态 - 为null。
对于这个问题我选择的解决方式是在工作方法中去初始化_bag变量。
public class Worker
{
[ThreadStatic] private static Bag _bag; public void PutTenApple()
{
_bag = new Bag(); //调用前初始化
for (int i = 0; i < 10; i++)
{
PutApple();
Show();
Thread.Sleep(100);
}
} private void PutApple()
{
++_bag.AppleNum;
} private void Show()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {_bag.AppleNum}");
}
}
ReSharper网站给出的方法是通过一个属性去包装这个静态字段,并将对静态字段的访问都换成对静态属性的访问。
public class Worker
{
[ThreadStatic] private static Bag _bag; public static Bag Bag => _bag ?? (_bag = new Bag()); public void PutTenApple()
{
for (int i = 0; i < 10; i++)
{
PutApple();
Show();
Thread.Sleep(100);
}
} private void PutApple()
{
++Bag.AppleNum;
} private void Show()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {Bag.AppleNum}");
}
}
对于线程本地变量,如果在线程外访问,会发现它并没有受到线程操作的影响。
public void TryTwoThread()
{ var worker = new Worker();
Parallel.Invoke(worker.PutTenApple, worker.PutTenApple); Console.WriteLine($"Main Thread : {Thread.CurrentThread.ManagedThreadId} - {Worker.Bag.AppleNum}");
}
主线程中访问情况:

往袋子里面装苹果 案例示范--数据曹
另一种等价的方法是使用LocalDataStoreSlot,但是性能不如上面介绍的ThreadStatic方法。
public class Worker
{
private LocalDataStoreSlot _localSlot = Thread.AllocateDataSlot(); public void PutTenApple()
{
Thread.SetData(_localSlot, new Bag()); for (int i = 0; i < 10; i++)
{
PutApple();
Show();
Thread.Sleep(100);
}
} private void PutApple()
{
var bag = Thread.GetData(_localSlot) as Bag;
++bag.AppleNum;
} private void Show()
{
var bag = Thread.GetData(_localSlot) as Bag;
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {bag.AppleNum}");
}
}
把线程相关的数据存储在LocalDataStoreSlot对象中,并通过Thread的GetData和SetData进行存取。
数据槽还有一种命名的分配方式:
private LocalDataStoreSlot _localSlot = Thread.AllocateNamedDataSlot("Apple");
public void PutTenApple()
{
_localSlot = Thread.GetNamedDataSlot("Apple");//演示用
Thread.SetData(_localSlot, new Bag());
for (int i = 0; i < 10; i++)
{
PutApple();
Show();
Thread.Sleep(100);
}
}
在多组件的情况下,用不同名称区分数据槽很有用。但如果不小心给不同组件起了相同的名字,则会导致数据污染。
数据槽的性能较低,微软也不推荐使用,而且不是强类型的,用起来也不太方便。
往袋子里面装苹果 案例示范--ThreadLocal
在.NET Framework 4以后新增了一种泛型化的本地变量存储机制 - ThreadLocal<T>。下面的例子也是在之前例子基础上修改的。对比之前代码就很好理解ThreadLocal<T>的使用,ThreadLocal<T>的构造函数接收一个lambda用于线程本地变量的延迟初始化,通过Value属性可以访问本地变量的值。IsValueCreated可以判断本地变量是否已经创建。
public class Worker
{
private ThreadLocal<Bag> _bagLocal = new ThreadLocal<Bag>(()=> new Bag(), true); public ThreadLocal<Bag> BagLocal => _bagLocal; public void PutTenApple()
{
if (_bagLocal.IsValueCreated) //在第一次访问后,线程本地变量才会被创建
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - 已初始化");
} for (int i = 0; i < 10; i++)
{
PutApple();
Show();
Thread.Sleep(100);
} if (_bagLocal.IsValueCreated)
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - 已初始化");
}
} private void PutApple()
{
var bag = _bagLocal.Value; //通过Value属性访问
++bag.AppleNum;
} private void Show()
{
var bag = _bagLocal.Value; //通过Value属性访问
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {bag.AppleNum}");
}
}
另外如果在初始化ThreadLocal<T>时,将其trackAllValues设置为true,则可以在使用ThreadLocal<T>的线程外部访问线程本地变量中所存储的值。如在测试代码中:
public void TryTwoThread()
{
var worker = new Worker();
Parallel.Invoke(worker.PutTenApple, worker.PutTenApple); // 可以使用Values在线程外访问所有线程本地变量(需要ThreadLocal初始化时将trackAllValues设为true)
foreach (var tval in worker.BagLocal.Values)
{
Console.WriteLine(tval.AppleNum);
}
}
转载自:https://www.cnblogs.com/lsxqw2004/p/6121889.html
【C# 线程】线程局部存储(TLS) 实战部分 ThreadStatic|LocalDataStoreSlot|ThreadLocal<T>的更多相关文章
- 【C# 线程】线程局部存储(TLS)理论部分 ThreadStatic|LocalDataStoreSlot|ThreadLocal<T>
线程本地存储(TLS:Thread Local Storage) 线程本地存储(Thread Local Storage),字面意思就是专属某个线程的存储空间.变量大体上分为全局变量和局部变量,一个进 ...
- 【windows核心编程】线程局部存储TLS
线程局部存储TLS, Thread Local Storage TLS是C/C++运行库的一部分,而非操作系统的一部分. 分为动态TSL 和 静态TLS 一.动态TLS 应用程序通过调用一组4个函数来 ...
- 线程局部存储tls的使用
线程局部存储(Thread Local Storage,TLS)主要用于在多线程中,存储和维护一些线程相关的数据,存储的数据会被关联到当前线程中去,并不需要锁来维护.. 因此也没有多线程间资源竞争问题 ...
- 线程本地存储TLS(Thread Local Storage)的原理和实现——分类和原理
原文链接地址:http://www.cppblog.com/Tim/archive/2012/07/04/181018.html 本文为线程本地存储TLS系列之分类和原理. 一.TLS简述和分类 我们 ...
- 线程本地存储TLS(Thread Local Storage)的原理和实现——分类和原理
本文为线程本地存储TLS系列之分类和原理. 一.TLS简述和分类 我们知道在一个进程中,所有线程是共享同一个地址空间的.所以,如果一个变量是全局的或者是静态的,那么所有线程访问的是同一份,如果某一个线 ...
- Linux线程 之 线程 线程组 进程 轻量级进程(LWP)
Thread Local Storage,线程本地存储,大神Ulrich Drepper有篇PDF文档是讲TLS的,我曾经努力过三次尝试搞清楚TLS的原理,均没有彻底搞清楚.这一次是第三次,我沉浸gl ...
- Linux线程的实现 & LinuxThread vs. NPTL & 用户级内核级线程 & 线程与信号处理
另,线程的资源占用可见:http://www.cnblogs.com/charlesblc/p/6242111.html 进程 & 线程的很多知识可以看这里:http://www.cnblog ...
- JAVA之旅(十五)——多线程的生产者和消费者,停止线程,守护线程,线程的优先级,setPriority设置优先级,yield临时停止
JAVA之旅(十五)--多线程的生产者和消费者,停止线程,守护线程,线程的优先级,setPriority设置优先级,yield临时停止 我们接着多线程讲 一.生产者和消费者 什么是生产者和消费者?我们 ...
- 常量,字段,构造方法 调试 ms 源代码 一个C#二维码图片识别的Demo 近期ASP.NET问题汇总及对应的解决办法 c# chart控件柱状图,改变柱子宽度 使用C#创建Windows服务 C#服务端判断客户端socket是否已断开的方法 线程 线程池 Task .NET 单元测试的利剑——模拟框架Moq
常量,字段,构造方法 常量 1.什么是常量 常量是值从不变化的符号,在编译之前值就必须确定.编译后,常量值会保存到程序集元数据中.所以,常量必须是编译器识别的基元类型的常量,如:Boolean ...
随机推荐
- 原子操作atomic解读
下面从一个问题引入: // ConsoleApplication5.cpp : 定义控制台应用程序的入口点. #include "stdafx.h" #include<ran ...
- Serverless计算
云服务的演化历程 整个it系统服务的搭建,随着时间有多个层级的演化.从最早的内部部署(On-premises) 到基于云的Iaas,Paas,Saas,Baas, Faas.服务的构建对开发者越来友好 ...
- 知识增强的预训练语言模型系列之KEPLER:如何针对上下文和知识图谱联合训练
原创作者 | 杨健 论文标题: KEPLER: A unified model for knowledge embedding and pre-trained language representat ...
- json模块 os模块 文件加密
目录 一:random随机模块 二:os模块 三:文件处理选择任意视频 四:sys模块 五:实现文件执行加密操作 六:json 序列化模块 七:json序列化 反序列化 八:json 文件写读方式 九 ...
- Python 单元测试 生产HTML测试报告
使用HTMLTestRunnerNew模块,生成单元测试的html报告,报告标题根据对应测试时间. import unittest from datetime import datetime from ...
- ApacheCN Python 译文集(二)20211110 更新
Python 应用计算思维 零.序言 第一部分:计算思维导论 一.计算机科学基础 二.计算思维要素 三.理解算法和算法思维 四.理解逻辑推理 五.探究性问题分析 六.设计解决方案和解决流程 七.识别解 ...
- ApacheCN PHP 译文集 20211101 更新
PHP 入门指南 零.序言 一.PHP 入门 二.数组和循环 三.函数和类 四.数据操作 五.构建 PHP Web 应用 六.搭建 PHP 框架 七.认证与用户管理 八.建立联系人管理系统 使用 PH ...
- ApacheCN JavaWeb 译文集 20211017 更新
使用 Spring5 构建 REST Web 服务 零.前言 一.一些基本知识 二.在 Spring5 中使用 Maven 构建 RESTfulWeb 服务 三.Spring 中的 Flux 和 Mo ...
- Android总结【不定期更新】
全屏显示: this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParam ...
- 有手就行3——持续集成环境—maven、tomcat、安装和配置
有手就行3--持续集成环境-maven.tomcat.安装 持续集成环境(5)-Maven安装和配置 持续集成环境(6)-Tomcat安装和配置 持续集成环境(5)-Maven安装和配置 在Jenki ...