实例级线程局部存储有哪些优点?

时间:2010-02-04 19:58:37

标签: java .net multithreading oop thread-local

This question让我对Java和.NET等高级开发框架中的thread-local storage感到好奇。

Java有ThreadLocal<T>类(可能还有其他构造),而.NET有data slots,很快就有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();
}

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

6 个答案:

答案 0 :(得分:11)

  

线程局部存储解决了什么问题,或者线程局部存储提供了哪些优于标准的面向对象的习惯,即创建单独的对象实例以包含线程本地数据?

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

至于优势与您的示例 - 如果您正在生成单个线程,那么使用线程本地存储而不是传入实例几乎没有优势。但是,当使用ThreadPool(直接或间接)工作时,ThreadLocal<T>和类似的结构变得非常有价值。

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

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

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

这消除了同步问题,但也使我们能够保持缓存。在这种情况下,总体收益非常大。

有关更具体的示例,请查看我在aggregation using the TPL上撰写的这篇博客文章。在内部,只要您使用ForEach overload that keeps local state(以及ThreadLocal<TLocal>方法),Parallel类就会使用Parallel.For<TLocal>。这是每个线程保持本地状态分开以避免锁定的方式。

答案 1 :(得分:6)

偶尔,有线程本地状态是有帮助的。一个示例是日志上下文 - 设置您当前正在服务的请求的上下文或类似内容可能很有用,这样您就可以整理所有日志以处理该请求。

另一个很好的例子是.NET中的System.Random。相当常见的知识是,每次要使用Random时都不应创建新实例,因此有些人创建单个实例并将其放在静态变量中...但这很尴尬,因为{{1} }不是线程安全的。相反,你真的想要每个线程一个实例,适当播种。 Random对此非常有用。

类似的例子是与线程相关的文化或安全上下文。

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

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

我会说,在大多数情况下,你可以将它变为静态 - 但偶尔你可能想要每个实例,每个线程的信息。同样,值得用你的判断来看看它有用的地方。

答案 2 :(得分:4)

它有助于将值传递给堆栈。当您需要调用堆栈中的值时,它很方便,但没有办法(或好处)将此值传递给作为方法参数所需的位置。上面将当前HttpRequest存储在ThreaLocal中的示例就是一个很好的例子:另一种方法是将HttpRequest作为参数传递到堆栈中,直到需要它。

答案 3 :(得分:3)

在Java中,线程本地存储在Web应用程序中非常有用,其中单个请求通常由给定的线程处理。以Spring Security为例,安全过滤器将执行身份验证,然后将用户凭据存储在Thread本地变量中。

这允许实际的请求处理代码能够访问当前用户请求/身份验证信息,而无需向代码注入任何其他内容。

答案 4 :(得分:1)

答案 5 :(得分:1)

您希望进行一系列调用,无处不在地访问某些变量。您可以在每次调用中将其作为参数传递

function startComputingA(other args) {
  global_v = create // declared locally
  call A2(other args, global_v)
  call A3(other args, global_v)

function A2(other args, global_v) {
  call A3(other args, global_v)

function A3(other args, global_v) {
  call A4(other args, global_v)

您的所有函数都必须声明global_v参数。这很糟糕。您具有存储全局变量的全局范围,并将其“虚拟地”路由到每个例程

variable global_v;
function A() { // use global_v and call B() }
function B() { // use global_v and call C() }

然而,可能会发生另一个线程同时开始执行其中一些功能的情况。这会破坏你的全局变量。因此,您希望变量对于所有例程全局可见,而不是在线程之间。您希望每个线程都有global_v的单独副本。这是本地存储必不可少的时候!您将global_v声明为线程局部变量。因此,任何线程都可以从任何地方访问global_v,但不同的副本。