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

关于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. sed 模式空间 保持空间

    sed之所以能以行为单位的编辑或修改文本,其原因在于它使用了两个空间:一个是活动的"模式空间(pattern space)",另一个是起辅助作用的"保持空间(hold s ...

  2. Simulink S-Function的使用(以串口接收MPU6050六轴陀螺仪参数为实例)

    S-Function 允许使用自定义C/C++函数作为传递函数,具有可移植性.也可以同样利用MATLAB函数进行相同的运算,看开发者熟悉程度而定. 项目流程 由系统串口接收数据包. 通过S-Funct ...

  3. Iptables的命令与用法

    目录 一:iptables的用法 1.iptables简介 二:Iptables链的概念 1.那四个表,有哪些作用? 2.那五条链,运行在那些地方? 3.Iptables流程图 三:iptables的 ...

  4. K8S SVC 转发原理

    在前面的文章中,我们已经多次使用到了 Service 这个 Kubernetes 里重要的服务对象.而 Kubernetes 之所以需要 Service,一方面是因为 Pod 的 IP 不是固定的,另 ...

  5. Spring源码-IOC部分-容器初始化过程【2】

    实验环境:spring-framework-5.0.2.jdk8.gradle4.3.1 Spring源码-IOC部分-容器简介[1] Spring源码-IOC部分-容器初始化过程[2] Spring ...

  6. ApacheCN 深度学习译文集 2020.9

    协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 不要担心自己的形象,只关心如何实现目标.--<原则>,生活原则 2.3.c 在线阅读 ApacheCN 面试求职交流群 72418 ...

  7. ApacheCN Angular 译文集 20211114 更新

    Angular 专家级编程 零.前言 一.架构概述和在 Angular 中构建简单应用 二.将 AngularJS 应用迁移到 Angular 应用 三.使用 Angular CLI 生成具有最佳实践 ...

  8. .exe文件自动重启

    echo  :杀死进程taskkill /f /im YYTWEB.exe  :等待10秒:ping 127.0.0.1 -n 10  start "" "D:\都江堰银 ...

  9. php include,require,include_once,require_once 的区别

    include(),require(),include_once(),require_once()作用都是包含并运行指定文件,但是使用场景又有很大区别. 1.include()和require()的区 ...

  10. DNS域名解析之反向解析and主从域名服务器 (今天大小便正常,未来可期)

    DNS解析之反向解析和域名主从服务器 反向解析:根据IP地址查找对应的域名 yum -y install bind 安装软件包 查看需要修改的配置文件所在路径 rpm -qc bind 查询bind软 ...