往袋子里面装苹果 错误案例示范

关于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中来实现与线程本地变量一样的效果,但那样在调用PutAppleShow方法时就免不了传参数。

下面开始介绍几种实现线程本地变量的方法。

往袋子里面装苹果  案例示范--线程相关的静态字段

第一种方法线程相关的静态字段是使用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对象中,并通过ThreadGetDataSetData进行存取。

数据槽还有一种命名的分配方式:

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>的更多相关文章

  1. 【C# 线程】线程局部存储(TLS)理论部分 ThreadStatic|LocalDataStoreSlot|ThreadLocal<T>

    线程本地存储(TLS:Thread Local Storage) 线程本地存储(Thread Local Storage),字面意思就是专属某个线程的存储空间.变量大体上分为全局变量和局部变量,一个进 ...

  2. 【windows核心编程】线程局部存储TLS

    线程局部存储TLS, Thread Local Storage TLS是C/C++运行库的一部分,而非操作系统的一部分. 分为动态TSL 和 静态TLS 一.动态TLS 应用程序通过调用一组4个函数来 ...

  3. 线程局部存储tls的使用

    线程局部存储(Thread Local Storage,TLS)主要用于在多线程中,存储和维护一些线程相关的数据,存储的数据会被关联到当前线程中去,并不需要锁来维护.. 因此也没有多线程间资源竞争问题 ...

  4. 线程本地存储TLS(Thread Local Storage)的原理和实现——分类和原理

    原文链接地址:http://www.cppblog.com/Tim/archive/2012/07/04/181018.html 本文为线程本地存储TLS系列之分类和原理. 一.TLS简述和分类 我们 ...

  5. 线程本地存储TLS(Thread Local Storage)的原理和实现——分类和原理

    本文为线程本地存储TLS系列之分类和原理. 一.TLS简述和分类 我们知道在一个进程中,所有线程是共享同一个地址空间的.所以,如果一个变量是全局的或者是静态的,那么所有线程访问的是同一份,如果某一个线 ...

  6. Linux线程 之 线程 线程组 进程 轻量级进程(LWP)

    Thread Local Storage,线程本地存储,大神Ulrich Drepper有篇PDF文档是讲TLS的,我曾经努力过三次尝试搞清楚TLS的原理,均没有彻底搞清楚.这一次是第三次,我沉浸gl ...

  7. Linux线程的实现 & LinuxThread vs. NPTL & 用户级内核级线程 & 线程与信号处理

    另,线程的资源占用可见:http://www.cnblogs.com/charlesblc/p/6242111.html 进程 & 线程的很多知识可以看这里:http://www.cnblog ...

  8. JAVA之旅(十五)——多线程的生产者和消费者,停止线程,守护线程,线程的优先级,setPriority设置优先级,yield临时停止

    JAVA之旅(十五)--多线程的生产者和消费者,停止线程,守护线程,线程的优先级,setPriority设置优先级,yield临时停止 我们接着多线程讲 一.生产者和消费者 什么是生产者和消费者?我们 ...

  9. 常量,字段,构造方法 调试 ms 源代码 一个C#二维码图片识别的Demo 近期ASP.NET问题汇总及对应的解决办法 c# chart控件柱状图,改变柱子宽度 使用C#创建Windows服务 C#服务端判断客户端socket是否已断开的方法 线程 线程池 Task .NET 单元测试的利剑——模拟框架Moq

    常量,字段,构造方法   常量 1.什么是常量 ​ 常量是值从不变化的符号,在编译之前值就必须确定.编译后,常量值会保存到程序集元数据中.所以,常量必须是编译器识别的基元类型的常量,如:Boolean ...

随机推荐

  1. Express框架的简单使用

    Express框架的简单使用 这个框架是基于Node.js的框架平台 需要先安装node.js 安装完node.js后使用指令操作 npm init --yes 初始化 npm i express 安 ...

  2. 带你学习BFS最小步数模型

    最小步数模型 一.简介 最小步数模型和最短路模型的区别? 最短路模型:某一个点到另一个点的最短距离(坐标与坐标之间) 最小步数模型:不再是点(坐标),而是状态到另一个状态的转变 BFS难点所在(最短路 ...

  3. 用Python实现一个Picgo图床工具

    PyPicGo PyPicGo 是一款图床工具,是PicGo是Python版实现,并支持各种插件自定义插件,目前PyPicGo自带了gitee.github.SM.MS和七牛云图传,以及rename. ...

  4. 主键约束(primary key 简称PK)

    7.5.主键约束 主键约束相关术语 主键约束 主键字段:字段添加了主键约束,叫主键字段 主键值:主键字段中的每个值都叫主键值 什么是主键? 主键值是每一行记录的唯一标识(主键值是每一行记录的身份证号) ...

  5. JavaScripts之迪卡算法求积(n*n)适用于SKU信息计算等场景

    迪卡算法求积(n * n) 使用 array.reduce 的方式实现 笛卡尔积算法 const arr = [ ['黑色', '白色', '蓝色'], ['1.2KG', '2.0KG', '3.0 ...

  6. Heartbeat部署

    部署环境:CentOS 7 1.Heartbeat介绍 Heartbeat是Linux-HA项目中的一个组件,它实现了一个高可用集群系统.心跳检测和集群通信是高可用的两个关键组件,在Heartbeat ...

  7. AT2164 [AGC006C] Rabbit Exercise

    首先我们可以考虑一下 \(x\) 关于 \(y\) 的对称点的坐标,不难发现就是 \(x + 2 \times (y - x)\),那么期望的增量就会增加 \(2 \times (y - x)\).不 ...

  8. DOM Document.readyState 属性

    感谢原文作者:MDN 原文地址:https://developer.mozilla.org/zh-CN/docs/Web/API/Document/readyState 描述 一个document 的 ...

  9. git merge -ff --no-ff --squash 的区别

    感谢原文作者:futureme 原文链接:https://www.cnblogs.com/taylorluo/articles/10810762.html git merge #没有参数(默认为–ff ...

  10. Eclipse不能启动,提示:The Eclipse executable launcher was unable to locate its companion launcher jar

    原因分析:JDK版本与eclipse不匹配 如jdk和eclipse版本号必须统一,64位都是64位,32位都是32位. jdk版本可以用命令,cmd进入命令窗口,然后输入java -version, ...