.Net学习难点讨论系列17 - 线程本地变量的使用
*:first-child {
  margin-top: 0 !important;
}
body>*:last-child {
  margin-bottom: 0 !important;
}
/* BLOCKS
=============================================================================*/
p, blockquote, ul, ol, dl, table, pre {
  margin: 15px 0;
}
/* HEADERS
=============================================================================*/
h1, h2, h3, h4, h5, h6 {
  margin: 20px 0 10px;
  padding: 0;
  font-weight: bold;
  -webkit-font-smoothing: antialiased;
}
h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
  font-size: inherit;
}
h1 {
  font-size: 28px;
  color: #000;
}
h2 {
  font-size: 24px;
  border-bottom: 1px solid #ccc;
  color: #000;
}
h3 {
  font-size: 18px;
}
h4 {
  font-size: 16px;
}
h5 {
  font-size: 14px;
}
h6 {
  color: #777;
  font-size: 14px;
}
body>h2:first-child, body>h1:first-child, body>h1:first-child+h2, body>h3:first-child, body>h4:first-child, body>h5:first-child, body>h6:first-child {
  margin-top: 0;
  padding-top: 0;
}
a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
  margin-top: 0;
  padding-top: 0;
}
h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
  margin-top: 10px;
}
/* LINKS
=============================================================================*/
a {
  color: #4183C4;
  text-decoration: none;
}
a:hover {
  text-decoration: underline;
}
/* LISTS
=============================================================================*/
ul, ol {
  padding-left: 30px;
}
ul li > :first-child,
ol li > :first-child,
ul li ul:first-of-type,
ol li ol:first-of-type,
ul li ol:first-of-type,
ol li ul:first-of-type {
  margin-top: 0px;
}
ul ul, ul ol, ol ol, ol ul {
  margin-bottom: 0;
}
dl {
  padding: 0;
}
dl dt {
  font-size: 14px;
  font-weight: bold;
  font-style: italic;
  padding: 0;
  margin: 15px 0 5px;
}
dl dt:first-child {
  padding: 0;
}
dl dt>:first-child {
  margin-top: 0px;
}
dl dt>:last-child {
  margin-bottom: 0px;
}
dl dd {
  margin: 0 0 15px;
  padding: 0 15px;
}
dl dd>:first-child {
  margin-top: 0px;
}
dl dd>:last-child {
  margin-bottom: 0px;
}
/* CODE
=============================================================================*/
pre, code, tt {
  font-size: 12px;
  font-family: Consolas, "Liberation Mono", Courier, monospace;
}
code, tt {
  margin: 0 0px;
  padding: 0px 0px;
  white-space: nowrap;
  border: 1px solid #eaeaea;
  background-color: #f8f8f8;
  border-radius: 3px;
}
pre>code {
  margin: 0;
  padding: 0;
  white-space: pre;
  border: none;
  background: transparent;
}
pre {
  background-color: #f8f8f8;
  border: 1px solid #ccc;
  font-size: 13px;
  line-height: 19px;
  overflow: auto;
  padding: 6px 10px;
  border-radius: 3px;
}
pre code, pre tt {
  background-color: transparent;
  border: none;
}
kbd {
    -moz-border-bottom-colors: none;
    -moz-border-left-colors: none;
    -moz-border-right-colors: none;
    -moz-border-top-colors: none;
    background-color: #DDDDDD;
    background-image: linear-gradient(#F1F1F1, #DDDDDD);
    background-repeat: repeat-x;
    border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD;
    border-image: none;
    border-radius: 2px 2px 2px 2px;
    border-style: solid;
    border-width: 1px;
    font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
    line-height: 10px;
    padding: 1px 4px;
}
/* QUOTES
=============================================================================*/
blockquote {
  border-left: 4px solid #DDD;
  padding: 0 15px;
  color: #777;
}
blockquote>:first-child {
  margin-top: 0px;
}
blockquote>:last-child {
  margin-bottom: 0px;
}
/* HORIZONTAL RULES
=============================================================================*/
hr {
  clear: both;
  margin: 15px 0;
  height: 0px;
  overflow: hidden;
  border: none;
  background: transparent;
  border-bottom: 4px solid #ddd;
  padding: 0;
}
/* TABLES
=============================================================================*/
table th {
  font-weight: bold;
}
table th, table td {
  border: 1px solid #ccc;
  padding: 6px 13px;
}
table tr {
  border-top: 1px solid #ccc;
  background-color: #fff;
}
table tr:nth-child(2n) {
  background-color: #f8f8f8;
}
/* IMAGES
=============================================================================*/
img {
  max-width: 100%
}
-->
关于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个苹果。当然还有其它并发控制方法,但那不是本文重点忽略不说。

在某些场景下我们会有另一种需求,我们关心的是每个线程往袋子里放了多少个苹果。这时我们就需要让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);
    }
}
在多组件的情况下,用不同名称区分数据槽很有用。但如果不小心给不同组件起了相同的名字,则会导致数据污染。
数据槽的性能较低,微软也不推荐使用,而且不是强类型的,用起来也不太方便。
.NET 4 - 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);
    }
}
关于线程本地变量就写到这吧。欢迎大家指正补充。
.Net学习难点讨论系列17 - 线程本地变量的使用的更多相关文章
- 线程本地变量ThreadLocal源码解读
		一.ThreadLocal基础知识 原始线程现状: 按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步.但是Spring中的各种模板 ... 
- 线程本地变量ThreadLocal
		一.本地线程变量使用场景 并发应用的一个关键地方就是共享数据.如果你创建一个类对象,实现Runnable接口,然后多个Thread对象使用同样的Runnable对象,全部的线程都共享同样的属性.这意味 ... 
- ThreadLocal 线程本地变量 及 源码分析
		■ ThreadLocal 定义 ThreadLocal通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量 ... 
- .Net - 线程本地变量(存储)的使用
		关于C#多线程的文章,大部分都在讨论线程的开始与停止或者是多线程同步问题.多线程同步就是在不同线程中访问同一个变量或共享资源,众所周知在不使用线程同步的机制下,由于竞争的存在会使某些线程产生脏读或者是 ... 
- Threadlocal线程本地变量理解
		转载:https://www.cnblogs.com/chengxiao/p/6152824.html 总结: 作用:ThreadLocal 线程本地变量,可用于分布式项目的日志追踪 用法:在切面中生 ... 
- linux TLS 线程本地变量
		最近在写底层hook的时候, 涉及到线程安全问题, 最开始我设计的时候使用的互斥量, 但是考虑到都是底层函数,加锁会导致性能问题, 一直在思考优化方案, 后来偶然想到,java里面有线程本地变量的AP ... 
- 线程本地变量ThreadLocal (耗时工具)
		线程本地变量类 package king; import java.util.ArrayList; import java.util.List; import java.util.Map; impor ... 
- 线程本地变量ThreadLocal (耗时工具)【原】
		线程本地变量类 package king; import java.util.ArrayList; import java.util.List; import java.util.Map; impor ... 
- java线程本地变量
		ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量).也许把它命名为Thre ... 
随机推荐
- 鼠标滚轮事件 onmousewheel
			1.fiefox:DOMMouseScroll detail detail*(-40)=wheelDelta 除了firefox之外:mousewheel ... 
- 创建表结构相同的表,表结构相同的表之间复制数据,Oracle 中 insert into XXX select from 的用法
			/**1. 用select 创建相同表结构的表*/create table test_tbl2 as select * from test_tbl1 where 1<>1; /** 2. ... 
- 如何使用XE2及更高版本中提供的自定义皮肤(样式)功能
			源:如何使用XE2及更高版本中提供的自定义皮肤(样式)功能 1. 制作样式文件: 点击 XE2+ 的 IDE 菜单上的 Tools-->Bitmap Style Designer, 打开设计器. ... 
- flume-1.6.0单节点部署
			这个不多说,直接上干货,部署很简单! 步骤一:flume的下载 当然,这里也可以使用wget命令在线下载,很简单,不多说. 步骤二:flume的上传 [hadoop@djt002 flume]$ ls ... 
- memcached 第二篇----安装使用
			摘要:set add replace get delete gets cas stats 和 flush_all 命令 获取所有key .你可以使用MemCachedClient的statsItem ... 
- Objective C HMAC-MD5
			- (NSString*) HMACWithSecret:(NSString*) secret andString:(NSString *)str { unsigned long encode = C ... 
- Spring 工作原理
			1.spring原理 内部最核心的就是IOC了,动态注入,让一个对象的创建不用new了,可以自动的生产,这其实就是利用java里的反射,反射其实就是在运行时动态的去创建.调用对象,Spring就是在运 ... 
- JQuery的 jQuery.fn.extend() 和jQuery.extend();
			原文链接:http://caibaojian.com/jquery-extend-and-jquery-fn-extend.html jQuery.fn.extend(); jQuery.extend ... 
- Nodejs之模板ejs
			ejs使用说明及介绍. 1.创建ejs项目 express -e test //创建模板为ejs的项目,默认为jade. 2.使用 app.js中添加 var ejs = require('ejs' ... 
- 刪除預裝在windows 10 的app
			刪除預裝在windows 10 的app 步驟: 方法一.(易於解除安裝的app) 1. →按"開始標誌" →"所有應用程式" →在想解除的程式圖示上" ... 
