【问题标题】:Parallel: lifetime of ThreadStatic properties (.NET)并行:ThreadStatic 属性的生命周期 (.NET)
【发布时间】:2017-08-09 10:01:18
【问题描述】:

我想了解在 .NET 中使用并行处理时 ThreadStatic 数据何时被清除。

考虑以下(大幅缩减的)代码:

我的上下文类

public class AppContext
{
    [ThreadStatic]
    private static Person _person;

    public Person Shopper
    {
        get => AppContext._person;
        set => AppContext._person = value;
    }
}
  • 这个类会有很多属性 - 肯定超过 50 个。
  • 每个属性的支持字段都是 ThreadStatic 字段。

大量对象的并行处理

var response = await Get100ItemsToProcess().ConfigureAwait(false);
var singleContext = new AppContext();

Parallel.ForEach(response.items, new ParallelOptions(), i =>
{
    // Set all properties on the singleContext for this thread.
    singleContext.Shopper = new Shopper { Name = ...., etc}
    ....

    ProcessItem(i);

    // Dispose of any IDisposable properties on the singleContext
    ....
});

注意:

  • ProcessItem(...) 不是一个简单的函数,而是一个复杂的、多步骤的同步过程,几乎是一个“应用程序中的应用程序”。因为它是同步的,我们可以使用 ThreadStatic 属性来保存特定于正在处理的项目的数据。

  • 当线程(例如 managedThreadId = 24)第一次进入并行循环时,singleContext.Shopper 最初为 null。

  • 当该线程 (managedThreadId = 24) 初始化 Shopper 时,该 Shopper 被保存在 ThreadStatic 字段中,因此其他线程无法访问。

  • 下一次同一个线程 (managedThreadId = 24) 重新进入循环(以处理不同的项目)时,singleContext.Shopper 仍然是在前一个循环期间实例化的同一个对象。

所以,我的理解(如有错误请指正)是:

  • 当我们创建一个 Parallel.ForEach 循环时,它会从线程池中分配一些线程
  • 当其中一个线程完成一个循环,然后开始一个新循环时,它的堆栈不会被清理 - 在第一次迭代中设置的所有 ThreadStatic 变量都保留在第二次迭代中(尽管我们重写了它们)
  • 只有当 Parallel.ForEach 完成时,线程才会返回到线程池中

因此,我的问题是:这些属性何时从内存中删除?当线程返回到 ThreadPool 时(大概是在 Parallel.ForEach 完成时),或者稍后将线程分配给不同的 AppDomain 时,它们是否被清理?我问是因为其中一些属性可能会消耗大量内存,我想确保它们不会占用内存超过他们需要的时间。我不是特别喜欢在每次循环迭代结束时将它们显式设置为 null 的想法...

【问题讨论】:

  • 我不确定 AppContext 类是为了解决什么问题,但我希望让一切都更加本地化,​​并考虑 ThreadLocal<T> 而不是 @987654325 @。您可以在使用后明确Dispose(或将其包装在using 中),然后您将清楚地了解生命周期。
  • @Damien_The_Unbeliever:AppContext 类包含开始处理项目所需的所有信息。如果它是一个网站,它可能是关于购物者或网站本身的信息。这是获取常用数据的“一站式”商店。我想它也有点像缓存,但在架构上有所不同。
  • @Damien_The_Unbeliever:关于使用 ThreadLocal 的要点很好,我们可能会在稍后讨论。为了尽量减少重构,我们需要保留单个 AppContext 的概念,但让每个属性的支持字段都有一个 ThreadLocal。这需要我们在 Parallel 循环中调用 .Dispose() 来处理当前线程的值。
  • 无论我们使用的是 ThreadStatic 还是 ThreadLocal,并行循环的每个后续迭代仍然保留该托管线程的上一次迭代的值。我对这些何时从内存中释放的理解仍然不清楚。
  • 我在您发布的一小段代码中没有看到任何证明使用[ThreadStatic]ThreadLocal<T>AsyncLocal<T> 中的any 是合理的。对于瞬态线程,通常正确的方法是将适当的上下文对象传递给线程,或者在这样的对象中实现线程(即隐式传递this)。所有这些其他机制都更重,并且具有这里似乎不需要的语义。不幸的是,您提出的问题过于宽泛,缺乏足够的背景信息,任何人都无法猜测您真正需要的答案。

标签: c# multithreading parallel-processing task-parallel-library


【解决方案1】:

ThreadStatic是线程专用的存储空间。

ThreadStatic 属性的生命周期

ThreadStatic是线程专用的存储空间,所以ThreadStatic属性对象的生命周期就是线程的生命周期。

当我们创建一个 Parallel.ForEach 循环时,它会从线程池中分配一些线程

Parallel 比这更复杂。它可以在执行期间根据需要调整它使用的线程数。线程可以在运行时“进入”和“离开”并行循环的“所有权”。

当其中一个线程完成一个循环,然后开始一个新循环时,它的堆栈不会被清理 - 在第一次迭代中设置的所有 ThreadStatic 变量都保留在第二次迭代中

是的。它与“堆栈”没有任何关系。 ThreadStatic是线程专用的存储空间,所以还是跟线程相关的。

这些属性何时从内存中删除?

ThreadStatic 是线程专用的存储空间,所以在线程完成后会被清理掉。

当线程返回到 ThreadPool 时(大概是 Parallel.ForEach 完成时)它们是否被清理?

没有。线程仍然存在,所以ThreadStatic 属性对象也仍然存在。

我并不特别喜欢在每次循环迭代结束时将它们显式设置为 null 的想法

Peter Duniho 说得对:“在您发布的一小段代码中,我没有看到任何证明使用 [ThreadStatic]ThreadLocal<T>AsyncLocal<T> 是合理的。对于瞬态线程,通常是正确的方法是只将适当的上下文对象传递给线程......所有这些其他机制都更加重量级,并且具有这里似乎不需要的语义。”

【讨论】:

  • 您在整个答案中指的是线程静态“对象”,但对象不是线程静态的,变量是,所以“对象”的大多数用法应该是“变量”。
  • @Stephen Cleary:好的,只要线程与我的应用程序(此进程的线程池)保持关联,它就会保留此数据。谢谢,这回答了我的问题。关于 Peter 的 Duniho 评论,我提到“ProcessItem()”方法是一个迷你应用程序。实际上,它正在调用一个现有应用程序的库,其架构需要这种方法,不幸的是,我无法更改它。
猜你喜欢
  • 1970-01-01
  • 2017-04-21
  • 1970-01-01
  • 2016-10-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-04
相关资源
最近更新 更多