实例级线程本地存储有哪些优势?

2022-09-02 12:55:41

这个问题使我想知道Java和.NET等高级开发框架中的线程本地存储

Java有一个ThreadLocal<T>类(也许还有其他构造),而.NET有数据槽,很快还有一个ThreadLocal<T>自己的类。(它也有ThreadStaticAttribute,但我对成员数据的线程本地存储特别感兴趣。大多数其他现代开发环境在语言或框架级别为其提供了一个或多个机制。

线程本地存储解决了哪些问题,或者线程本地存储与创建单独的对象实例以包含线程本地数据的标准面向对象习惯用法相比提供了哪些优势?换句话说,这是怎么回事:

// Thread local storage approach - start 200 threads using the same object
// Each thread creates a copy of any thread-local data
ThreadLocalInstance instance = new ThreadLocalInstance();
for(int i=0; i < 200; i++) {
    ThreadStart threadStart = new ThreadStart(instance.DoSomething);
    new Thread(threadStart).Start();
}

比这更优越吗?

// Normal oo approach, create 200 objects, start a new thread on each
for(int i=0; i < 200; i++) {
    StandardInstance standardInstance = new StandardInstance();
    ThreadStart threadStart = new ThreadStart(standardInstance.DoSomething);      
    new Thread(threadStart).Start();
}

我可以看到,使用具有线程本地存储的单个对象可以稍微提高内存效率,并且由于更少的分配(和构造)而需要更少的处理器资源。还有其他优势吗?


答案 1

线程本地存储解决了哪些问题,或者线程本地存储与创建单独的对象实例以包含线程本地数据的标准面向对象习惯用法相比提供了哪些优势?

线程本地存储允许您为每个正在运行的线程提供类的唯一实例,这在尝试使用非线程安全类或尝试避免由于共享状态而可能发生的同步要求时非常有用。

至于与你的示例相比的优势 - 如果您要生成单个线程,则与在实例中传递相比,使用线程本地存储几乎没有优势。 然而,当(直接或间接地)使用ThreadPool时,类似的结构变得非常有价值。ThreadLocal<T>

例如,我最近处理了一个特定的过程,我们正在使用.NET中的新任务并行库进行一些非常繁重的计算。可以缓存执行的计算的某些部分,如果缓存包含特定的匹配项,我们可以在处理一个元素时节省相当多的时间。但是,缓存的信息具有较高的内存要求,因此我们不希望缓存超过上一个处理步骤。

但是,尝试在线程之间共享此缓存是有问题的。为此,我们必须同步对它的访问,并在我们的类中添加一些额外的检查,以使它们线程安全。

我没有这样做,而是重写了算法,以允许每个线程在.这允许每个线程维护自己的专用缓存。由于 TPL 使用的分区方案倾向于将元素块保持在一起,因此每个线程的本地缓存都倾向于包含所需的适当值。ThreadLocal<T>

这消除了同步问题,但也允许我们将缓存保留在原位。在这种情况下,总体效益相当大。

有关更具体的示例,请查看我写的有关使用 TPL 进行聚合的博客文章。在内部,Parallel 类使用 ForEach 重载时都会使用 ,该重载保持本地状态(以及方法)。这就是每个线程将本地状态保持为单独的方式,以避免锁定。ThreadLocal<TLocal>Parallel.For<TLocal>


答案 2

只是偶尔,拥有线程局部状态会很有帮助。一个示例是日志上下文 - 设置当前正在处理哪个请求的上下文或类似内容可能很有用,以便您可以整理与该请求相关的所有日志。

另一个很好的例子是.NET。众所周知,您不应该每次都使用时都创建新实例,因此有些人会创建单个实例并将其放在静态变量中...但这很尴尬,因为不是线程安全的。相反,您确实希望每个线程有一个实例,并适当地设定种子。 效果很好。System.RandomRandomRandomThreadLocal<T>

类似的示例包括与线程关联的区域性或安全上下文。

一般来说,这是一个不想到处传递太多上下文的情况。你可以让每个方法调用都包含一个“RandomContext”或“LogContext” - 但它会妨碍你的API的清洁度 - 如果你不得不调用另一个API,这个API将通过虚拟方法或类似的东西回调到你的API,那么链条就会被打破。

在我看来,线程本地数据应该尽可能避免 - 但偶尔它可能非常有用。

我想说的是,在大多数情况下,你可以摆脱它是静态的 - 但偶尔你可能想要每个实例,每个线程的信息。同样,值得使用您的判断来查看其有用之处。