18

这个问题让我想知道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();
}

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

4

6 回答 6

12

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

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

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

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

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

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

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

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

于 2010-02-04T22:37:05.340 回答
6

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

另一个很好的例子是System.Random.NET。众所周知,您不应该每次要使用时都创建一个新实例Random,因此有些人创建一个实例并将其放入静态变量中……但这很尴尬,因为Random它不是线程安全的。相反,您确实希望每个线程一个实例,并适当地播种。ThreadLocal<T>非常适合这个。

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

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

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

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

于 2010-02-04T20:02:24.057 回答
4

它有助于将值向下传递到堆栈。当您需要调用堆栈中的值但没有办法(或好处)将此值传递到需要它作为方法参数的位置时,它会派上用场。上面将当前 HttpRequest 存储在 ThraLocal 中的示例就是一个很好的例子:替代方法是将 HttpRequest 作为参数向下传递到堆栈到需要它的位置。

于 2010-02-04T20:15:27.270 回答
3

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

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

于 2010-02-04T20:02:40.463 回答
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从任何地方访问,但它的不同副本。

于 2013-01-10T16:08:58.200 回答
1

这是 ThreadLocal 的实际用法:http: //blogs.captechconsulting.com/blog/balaji-muthuvarathan/persistence-pattern-using-threadlocal-and-ejb-interceptors

于 2010-07-15T15:29:29.697 回答