关于C#多线程的文章,大部分都在讨论线程的开始与停止或者是多线程同步问题。多线程同步就是在不同线程中访问同一个变量或共享资源,众所周知在不使用线程同步的机制下,由于竞争的存在会使某些线程产生脏读或者是覆盖其它线程已写入的值(各种混乱)。
而另外一种情况就是多线程时我们想让每个线程所访问的变量只属于各自线程自身所有,这就是所谓的线程本地变量。
线程本地变量不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制,理解这点对正确使用线程本来变量至关重要, 通过线程本地变量存取的数据,总是与当前线程相关。
本文重点介绍几种线程本地变量的存储方式,并简单介绍一下线程并发访问各自解决方案。
多线程同步
public class Test { private static 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); } }
最容易的方法就是给共享变量的访问加个锁来解决并发问题,当然如果在分布式系统中可以使用分布式锁。上例执行结果如下图:
线程本地变量
下面介绍NET下三种线程本地存储(Thread-Local Storage)方法:ThreadStatic, LocalDataStoreSlot 和ThreadLocal
1. 使用ThreadStatic特性
线程相关的静态字段,其做法是将成员变量声明为static
并打上[ThreadStatic]
这个标记。ThreadStatic特性是最简单的TLS使用,且只支持静态字段,只需要在字段上标记这个特性就可以了
//TLS中的str变量 [ThreadStatic] private static string str = "hehe"; static void Main() { //另一个线程只会修改自己TLS中的str变量 Thread th = new Thread(() => { str = "Mgen"; Display(); }); th.Start(); th.Join(); Display(); } static void Display() { Console.WriteLine("{0} {1}", Thread.CurrentThread.ManagedThreadId, str); }
运行结果:
3 Mgen 1 hehe
可以看到,str静态字段在两个线程中都是独立存储的,互相不会被修改。
2. 数据槽
显然ThreadStatic特性只支持静态字段太受限制了,.NET线程类型中的LocalDataStoreSlot提供更好的TLS支持,但是性能不如上面介绍的ThreadStatic方法。注意:LocalDataStoreSlot有命名类型和非命名类型区分。
我们先来看看命名的LocalDataStoreSlot类型,可以通过Thread.AllocateNamedDataSlot来分配一个命名的空间,通过Thread.FreeNamedDataSlot来销毁一个命名的空间。
把线程相关的数据存储在LocalDataStoreSlot对象中,空间数据的获取和设置则通过Thread类型的GetData方法和SetData方法。
static void Main() { //创建Slot LocalDataStoreSlot slot = Thread.AllocateNamedDataSlot("slot"); //设置TLS中的值 Thread.SetData(slot, "hehe"); //修改TLS的线程 Thread th = new Thread(() => { Thread.SetData(slot, "Mgen"); Display(); }); th.Start(); th.Join(); Display(); //清除Slot Thread.FreeNamedDataSlot("slot"); } //显示TLS中Slot值 static void Display() { LocalDataStoreSlot dataslot = Thread.GetNamedDataSlot("slot"); Console.WriteLine("{0} {1}", Thread.CurrentThread.ManagedThreadId, Thread.GetData(dataslot)); }
运行结果:
3 Mgen 1 hehe
在多组件的情况下,用不同名称区分数据槽很有用。但如果不小心给不同组件起了相同的名字,则会导致数据污染。
线程同样支持未命名的LocalDataStoreSlot,未命名的LocalDataStoreSlot不需要手动清除,分配则需要Thread.AllocateDataSlot方法。
注意由于未命名的LocalDataStoreSlot没有名称,因此无法使用Thread.GetNamedDataSlot方法,只能在多个线程中引用同一个LocalDataStoreSlot才可以对TLS空间进行操作,将上面的命名的LocalDataStoreSlot代码改成未命名的LocalDataStoreSlot执行:
//静态LocalDataStoreSlot变量 private static LocalDataStoreSlot slot; static void Main() { //创建Slot slot = Thread.AllocateDataSlot(); //设置TLS中的值 Thread.SetData(slot, "hehe"); //修改TLS的线程 Thread th = new Thread(() => { Thread.SetData(slot, "Mgen"); Display(); }); th.Start(); th.Join(); Display(); } //显示TLS中Slot值 static void Display() { Console.WriteLine("{0} {1}", Thread.CurrentThread.ManagedThreadId, Thread.GetData(slot)); }
输出和上面的类似。
数据槽的性能较低,微软也不推荐使用,而且不是强类型的,用起来也不太方便。另外LocalDataStoreSlot不可能有默认值,因为初始化只能构造一个空间
3. .NET 4.0的ThreadLocal类型
在.NET Framework 4.0以后新增了一种泛型化的本地变量存储机制 - ThreadLocal
。他的出现更大的简化了TLS的操作,下面的例子也是在之前例子基础上修改的。
对比之前代码就很好理解ThreadLocal
的使用,ThreadLocal
的构造函数接收一个lambda用于线程本地变量的延迟初始化,通过Value属性可以访问本地变量的值。IsValueCreated可以判断本地变量是否已经创建。
private static ThreadLocal<string> local;
public ThreadLocal<string> BagLocal;
static void Main() { //创建ThreadLocal并提供默认值 local = new ThreadLocal<string>(() => "hehe", true);
BagLocal = local; //修改TLS的线程 Thread th = new Thread(() => { local.Value = "Mgen"; //通过Value属性访问 Display(); }); th.Start(); th.Join(); Display(); } //显示TLS中数据值 static void Display() { Console.WriteLine("{0} {1}", Thread.CurrentThread.ManagedThreadId, local.Value); }
运行结果:
3 Mgen 1 hehe
另外如果在初始化ThreadLocal
时,将其trackAllValues设置为true,则可以在使用ThreadLocal
的线程外部访问线程本地变量中所存储的值。如在测试代码中:
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); } }