线程本地变量的使用

Net学习难点讨论系列17 - 线程本地变量的使用

关于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中来实现与线程本地变量一样的效果,但那样在调用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);
    }
}

在多组件的情况下,用不同名称区分数据槽很有用。但如果不小心给不同组件起了相同的名字,则会导致数据污染。
数据槽的性能较低,微软也不推荐使用,而且不是强类型的,用起来也不太方便。

.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);
    }
}

关于线程本地变量就写到这吧。欢迎大家指正补充。

时间: 2024-10-25 07:15:11

线程本地变量的使用的相关文章

.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; } /* HEAD

线程本地变量ThreadLocal

一.本地线程变量使用场景 并发应用的一个关键地方就是共享数据.如果你创建一个类对象,实现Runnable接口,然后多个Thread对象使用同样的Runnable对象,全部的线程都共享同样的属性.这意味着,如果你在一个线程里改变一个属性,全部的线程都会受到这个改变的影响. 有时,你希望程序里的各个线程的属性不会被共享. Java 并发 API提供了一个很清楚的机制叫本地线程变量即ThreadLocal. 模拟ThreadLocal类实现:线程范围内的共享变量,每个线程只能访问他自己的,不能访问别的

线程本地变量ThreadLocal源码解读

  一.ThreadLocal基础知识   原始线程现状: 按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步.但是Spring中的各种模板类并未采用线程同步机制,因为线程同步会影响并发性和系统性能,而且实现难度也不小. ThreadLocal在Spring中发挥着重要的作用.在管理request作用域的bean,事务管理,任务调度,AOP等模块中都出现了它的身影. ThreadLocal介绍: 它不是一个线程,而是线程的一个本地化

java线程本地变量

ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量).也许把它命名为ThreadLocalVar更加合适.线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突. 从线程的角度看,每个线程都保持一个对其线程局部变量副本

Java并发机制(4)--ThreadLocal线程本地变量(转)

转自:博客园-海子-http://www.cnblogs.com/dolphin0520/p/3920407.html Java并发编程:深入剖析ThreadLocal 首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的.各个线程中访问的是不同的对象. 另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal

.Net - 线程本地变量(存储)的使用

关于C#多线程的文章,大部分都在讨论线程的开始与停止或者是多线程同步问题.多线程同步就是在不同线程中访问同一个变量或共享资源,众所周知在不使用线程同步的机制下,由于竞争的存在会使某些线程产生脏读或者是覆盖其它线程已写入的值(各种混乱). 而另外一种情况就是多线程时我们想让每个线程所访问的变量只属于各自线程自身所有,这就是所谓的线程本地变量. 线程本地变量不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制,理解这点对正确使用线程本来变量至关

深入理解线程本地变量ThreadLocal

ThreadLocal理解: 如果在多线程并发环境中,一个可变对象涉及到共享与竞争,那么该可变对象就一定会涉及到线程间同步操作,这是多线程并发问题. 否则该可变对象将作为线程私有对象,可通过ThreadLocal进行管理,实现线程间私有对象隔离的目的. 可以发现,ThreadLocal并没有解决多线程并发的问题,因为ThreadLocal管理的可变对象的性质本来就不会涉及到多线程并发而引发的共享.竞争和同步问题,使用ThreadLocal管理只是方便了多线程获取和使用该私有可变对象的途径和方式.

JAVA线程本地变量ThreadLocal和私有变量的区别

ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些. 所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及. ThreadLocal的接口方法 ThreadLocal类接口很简单,只有4个方法,我们先来了解一下: void set(Object value) public void remove() 将当前线程局部变量的值删除,目的是为了减

并发组件之一:ThreadLocal线程本地变量

一.简介  ThreadLocal从字面上进行理解很容易被大部分人认为是本地线程,然而ThreadLocal并不是一个Thread,可以说它只是一个容器,而它装的内容又是Thread的局部变量.很多文章都会把ThreadLocal当作是解决高并发下线程不安全的一种做法,然而ThreadLocal并不是为了解决并发安全甚至可以这么说,它与真正的并发安全背道而驰.并发安全是指多个线程对同一个对象进行操作而导致的不安全,但是ThreadLocal在每个线程内部保存了一份该对象,使得每个线程都操作自己内