.NET 线程独享的全局数据 TLS

线程和进程最大的一个区别就在于线程间可以共享数据和资源,而进程则充分地隔离。在很多场合,即使同一进程的多个线程之间拥有相同的内存空间,也需要在逻辑上为某些线程分配独享的数据。例如,在实际开发中往往会针对一些ORM如EF一类的上下文实体做线程内唯一实例的设置,这时就需要用到下面提到的技术。

(1)线程本地存储(Thread Local Storage,TLS)

很多时候,程序员可能会希望拥有线程内可见的变量,而不希望其他线程对其进行访问和修改(传统方式中的静态变量是对整个应用程序域可见的),这就需要用到TLS的概念。所谓的线程本地存储(TLS)是指存储在线程环境块内的一个结构,用来存放该线程内独享的数据。进程内的线程不能访问不属于自己的TLS,这就保证了TLS内的数据在线程内是全局共享的,而对于线程外确实不可见的。

(2)定义和使用TLS变量

在.NET中提供了下列连个方法来存取线程独享的数据,它们都定义在System.Threading.Thread类型中:

① object GetData(LocalDataStoreSlot slot)

② void SetData(LocalDataStoreSlot slot, object data)

下面的代码示例则展示了这个机制的使用方法:

class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("开始测试数据插槽:");
            // 创建五个线程来同时运行,但是这里不适合用线程池,
            // 因为线程池内的线程会被反复使用导致线程ID一致
            for (int i = 0; i < 5; i++)
            {
                Thread thread = new Thread(ThreadDataSlot.Work);
                thread.Start();
            }

            Console.ReadKey();
        }
    }

    /// <summary>
    /// 包含线程方法和数据插槽
    /// </summary>
    public class ThreadDataSlot
    {
        // 分配一个数据插槽,注意插槽本身是全局可见的,因为这里的分配是在所有线程
        // 的TLS内创建数据块
        private static LocalDataStoreSlot localSlot = Thread.AllocateDataSlot();

        // 线程要执行的方法,操作数据插槽来存放数据
        public static void Work()
        {
            // 将线程ID注册到数据插槽中,一个应用程序内线程ID不会重复
            Thread.SetData(localSlot, Thread.CurrentThread.ManagedThreadId);
            // 查看一下刚刚插入的数据
            Console.WriteLine("线程{0}内的数据是:{1}",Thread.CurrentThread.ManagedThreadId.ToString(),Thread.GetData(localSlot).ToString());
            // 这里线程休眠1秒
            Thread.Sleep(1000);
            // 查看其他线程的运行是否干扰了当前线程数据插槽内的数据
            Console.WriteLine("线程{0}内的数据是:{1}", Thread.CurrentThread.ManagedThreadId.ToString(), Thread.GetData(localSlot).ToString());
        }
    }

在这里插入图片描述
(3)ThreadStaticAttribute特性的使用

除了使用上面说到的数据槽之外,我们还有另一种方式,即ThreadStaticAttribute特性。申明了该特性的变量,会被.NET作为线程独享的数据来使用。我们可以将其理解为一种被.NET封装了的TLS机制,本质上,它仍然使用了线程环境块来存放数据。

下面的示例代码展示了ThreadStaticAttribute特性的使用:
class Program
{
static void Main(string[] args)
{
Console.WriteLine(“开始测试数据插槽:”);
// 创建五个线程来同时运行,但是这里不适合用线程池,
// 因为线程池内的线程会被反复使用导致线程ID一致
for (int i = 0; i < 5; i++)
{
Thread thread = new Thread(ThreadStatic.Work);
thread.Start();
}

        Console.ReadKey();
    }
}

/// <summary>
/// 包含线程静态数据
/// </summary>
public class ThreadStatic
{
    // 值类型的线程静态数据
    [ThreadStatic]
    private static int threadId = 0;
    // 引用类型的线程静态数据
    private static Ref refThreadId = new Ref();

    /// <summary>
    /// 线程执行的方法,操作线程静态数据
    /// </summary>
    public static void Work()
    {
        // 存储线程ID,一个应用程序域内线程ID不会重复
        threadId = Thread.CurrentThread.ManagedThreadId;
        refThreadId.Id = Thread.CurrentThread.ManagedThreadId;
        // 查看一下刚刚插入的数据
        Console.WriteLine("[线程{0}]:线程静态值变量:{1},线程静态引用变量:{2}", Thread.CurrentThread.ManagedThreadId.ToString(), threadId, refThreadId.Id.ToString());
        // 睡眠1s
        Thread.Sleep(1000);
        // 查看其他线程的运行是否干扰了当前线程静态数据
        Console.WriteLine("[线程{0}]:线程静态值变量:{1},线程静态引用变量:{2}", Thread.CurrentThread.ManagedThreadId.ToString(), threadId, refThreadId.Id.ToString());
    }
}

/// <summary>
/// 简单引用类型
/// </summary>
public class Ref
{
    private int id;

    public int Id
    {
        get
        {
            return id;
        }
        set
        {
            id = value;
        }
    }
}

在这里插入图片描述


版权声明:本文为u013400314原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。