如何初始化一个线程安全的静态缓存反射数据

3
我已经思考了几个小时,发现这个任务有些具有挑战性。我正在尝试从基类初始化一个给定对象的静态反射数据缓存,以便它可以从多个线程进行访问。但是我很难想出正确的模式来初始化缓存。
我的第一个想法是只需将静态缓存初始化为null,在构造函数中检查它是否为null,如果不是,则构建并设置它。例如:
class TestBase
{
  private static ConcurrentDictionary<string, PropertyInfo> Cache;

  protected TestBase()
  {
    if(Cache == null)
    {
      ConcurrentDictionary<string, PropertyInfo> cache =
        new ConcurrentDictionary<string, PropertyInfo>();
      // Populate...
      Cache = cache;
    }
  }
}

这种方法存在一个缺陷,如果在第一个对象还在填充缓存时构造另一个对象,我将会构造两个缓存,第二个缓存会(大概率)覆盖第一个缓存。虽然这可能没问题,因为它们两个都是完整的缓存,但这似乎很糟糕,我希望我们能做得更好。
因此,我的第二个想法是在静态构造函数中初始化缓存,在创建任何实例之前,每个应用程序域只调用一次静态构造函数。StackOverflow上有几个类似问题的回答指向了这个方向。这看起来很不错,直到我意识到派生类型的反射数据对静态构造函数不可用。
我总是可以在构造函数中同步访问,以确保只有一个线程正在创建/填充缓存,而在此期间的任何其他访问都应该被阻塞,但是这样一来,我就只是为了保护一个只发生一次的操作而在每次构造时锁定。我不喜欢这样做的性能影响。
我现在的解决方案是使用Interlocked.Exchange和ManualResetEventSlim设置标志。它看起来像这样:
class TestBase
{
  private static ConcurrentDictionary<string, PropertyInfo> Cache;
  private static volatile int BuildingCache = 0;
  private static ManualResetEventSlim CacheBuilt =
    new ManualResetEventSlim();

  protected TestBase()
  {
    if(Interlocked.Exchange(ref BuildingCache, 1) == 0)
    {
      Cache = new ConcurrentDictionary<string, PropertyInfo>();
      // Populate...
      CacheBuilt.Set();
    }
    CacheBuilt.Wait();
  }
}

我怀疑已经有一种被接受或至少已知的方法来做这种事情-这是吗?如果不是,是否有更好的方法来同步缓存初始化?请注意,问题不是如何使缓存访问线程安全,可以通过使用ConcurrentDictionary(或类似)来假定。


我认为即使是只读的“get”,Dictionary<,>也是线程安全的。因此,一旦您的缓存准备就绪,我甚至不认为ConcurrentDictionary是必要的。如果您感兴趣,值得测试/研究。 - payo
我刚刚测试了静态构造函数的想法,并且派生部分的反射起作用了,你所说的“这听起来很棒,直到我意识到静态构造函数无法访问派生类型的反射数据。”是什么意思? - payo
@payo - 你是如何获取派生类型的Type实例引用的?在静态构造函数中没有“this”,因此无法使用this.GetType()。也许我表达得不够清楚 - 我不知道派生类型(而且不止一个)。也就是说,在基类的静态构造函数中无法执行Type type = typeof(SomeDerivedType);以构建缓存。 - daveaglick
你应该在构造函数之外创建字典(private static ConcurrentDictionary<string, PropertyInfo> Cache = new ConcurrentDictionary<string, PropertyInfo>();),并检查 Count 来确定是否需要构建缓存。 - JamieSee
@JamieSee - 我其实也考虑过这个问题,但我的担忧是在添加第一项后,任何后续的实例化都将被允许继续进行(因为至少有一个项目在缓存中),即使额外的缓存项目仍在第一个构造函数中添加。这可能会导致以下实例在缓存还在建设中时访问不完整的缓存。 - daveaglick
@somedave 如果你需要实例,你是如何传递它们的?根据你所做的事情,我可能有一个不错的解决方案。 - payo
2个回答

3
你可以使用Lazy<T>类在线程安全的情况下懒惰地初始化某些内容,而无需编写自己的代码。
如果您想要急切地缓存反射数据,则需要使用Assembly.GetTypes()来扫描兼容的类型(例如,用某些属性装饰的类型)。例如:
var types = typeof(TestBase).Assembly.GetTypes().Where(type => --some condition--);

太好了 - 我以前从未遇到过这个类。Lazy<T>.Value在值被初始化时是否会阻塞(我认为它必须这样做)?如果是这样,那看起来完美无缺。 - daveaglick
1
@somedave 默认情况下它会阻塞(尽管您可以更改此行为)。第一个线程初始化所有其他线程的所有内容。有关此及其他延迟初始化概念的更多信息,请参见MSDN上的“延迟初始化”(http://msdn.microsoft.com/en-us/library/dd997286.aspx)。 - Chris Hannon
@somedave:它总是阻塞,直到初始化完成。您可以使用接受 LazyThreadSafetyMode 枚举的构造函数重载来配置执行初始化例程的线程。 - Allon Guralnek
@ChrisHannon:它总是阻塞,并且这种行为是不可配置的。只有执行初始化的人是可配置的。此外,很酷的文章,我不知道还有其他两个类。 - Allon Guralnek
@ChrisHannon:如果你看一下somedave的第一条评论,你会发现他指的是“阻塞”的前一种解释,而不是后一种。 - Allon Guralnek
显示剩余2条评论

0

你应该在构造函数之外创建字典 (private static ConcurrentDictionary<string, PropertyInfo> Cache = new ConcurrentDictionary<string, PropertyInfo>();) 并检查 Count 属性以确定是否需要构建缓存。然后,如果在构建缓存时使用 TryAdd 来执行添加操作,你就不需要进行任何自己的线程锁定。

你可以通过使用另一个属性 IsCahceValid 来处理你在评论中提出的问题,以指示缓存已完成。由于你将使用 TryAdd,因此甚至不需要锁定变量,因为在任何初始完成点将其设置为 true 即可正常工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接