线程安全的一次性计算最佳实践

时间:2021-04-05 21:01:03

It's quite common that I need a property in my class which needs to be calculated and cached.

我需要一个需要计算和缓存的属性,这是很常见的。

Generally I use a lock and a boolean top check if it's processed or not. Sometimes I do it in accessors.

通常我会使用锁和布尔顶部检查是否已处理。有时我会在访问器中执行此操作。

What's the performance hit of this approach? Is there any better way to it.

这种方法的性能影响是什么?有没有更好的方法呢。

Sample Code of my common approach to this:

我常用方法的示例代码:

   Sub Main()
        Dim X AS New X()

        For i AS Integer = 0 To 50
            Dim Thr AS New Threading.Thread(ADdressOF X.ProcessData )
            Thr.Start()
        Next

    End Sub

Private Class X

    Private DataCached AS Boolean 
    Private ProcessedData AS String 
    Private Lock AS New Object()
    Public Function ProcessData() AS String

    Synclock Lock
        IF NOT DataCached Then
            DataCached = True
            ProcessedData = DoStuff()
        End If
    End Synclock

        Console.Writeline(ProcessedData)        
        Return ProcessedData
    End Function


    Function DoStuff() AS String 
        Threading.Thread.Sleep(1000)
        Console.Writeline("Processed")
        return "stuff"
    End Function

End Class

EDIT :

This is something that need to be calculated when accessed because it keeps changing. Constructor calculation doesn't help in here. (sample is a really simplified version of what I'm doing)

这是需要在访问时计算的东西,因为它不断变化。构造函数计算在这里没有帮助。 (样本是我正在做的一个非常简化的版本)

4 个解决方案

#1


Is it critical that it is never calculated twice? i.e. if two threads happened to ask for it at the same time, and calculate the value independently, is that a show-stopper? In most cases, it isn't - in which case, just check for null (since it is a string): (example in C#, apologies):

它永远不会被计算两次至关重要吗?即如果两个线程同时要求它,并独立计算该值,那么它是一个显示器吗?在大多数情况下,它不是 - 在这种情况下,只检查null(因为它是一个字符串):( C#中的例子,道歉):

   if(processedData == null) {
       processedData = DoStuff();
   }
   return processedData;

All subsequent calls should see the new value (I don't think we'll need volatile if it is hidden inside a property/method).

所有后续调用都应该看到新值(如果它隐藏在属性/方法中,我认为我们不需要volatile)。

This has the advantage of being lock-free and simple.

这具有无锁且简单的优点。

Another trick is to use a static property of a nested class:

另一个技巧是使用嵌套类的静态属性:

string SomeValue {
   get {return MyCache.SomeValue;}
}
static class MyCache {
    public static readonly string SomeValue;
    static MyCache() {
         SomeValue = DoStuff();
    }
}

This is calculated lazily, but the rules of static initializers mean that it is guaranteed to run once only (excluding reflection).

这是懒惰计算的,但静态初始化程序的规则意味着它只能运行一次(不包括反射)。

#2


You can improve the concurrency with a double-check optimization:

您可以通过双重检查优化来提高并发性:

If Not DataCached Then
    Synclock Lock
    If Not DataCached Then
        ProcessedData = DoStuff()
        DataCached = True ' Set this AFTER processing
    End If
End Synclock

This will avoid the critical section after the first init.

这将避免第一个init之后的关键部分。

#3


This is the only way, there could be some other system library to do this, but eventually that library also would do same thing internally.

这是唯一的方法,可能有一些其他系统库来做到这一点,但最终该库也会在内部做同样的事情。

#4


Firslty, I would move the caching outside of your class that contains the business logic and keep your business logic pure and allow you to control the caching independent of the application. But that's not your question ...

Firslty,我会在您的类之外移动包含业务逻辑的缓存,并保持您的业务逻辑纯净,并允许您独立于应用程序控制缓存。但那不是你的问题......

You need did not mention if you'll take more of a hit potentially calculating the things multiple times, until the cache is hot. The simple approach there would be:

你需要没有提到你是否会多次冒险,可能会多次计算这些东西,直到缓存很热。简单的方法是:

if (Cache["key"] == null)
  Cache["key"] = obj.processData();

return Cache["key"];

Cache itself should ensure that this is safe.

缓存本身应该确保这是安全的。

If you wish to explicitly block whilst the cache is being populated, then you already have the semantics to do so in your code above, however I recommend this change:

如果你希望在填充缓存时显式阻塞,那么你已经在上面的代码中有了这样做的语义,但是我推荐这个改变:

if (Cache["key"] == null) {
  Synclock blockIfProcessing
    if (Cache["key"] == null) 
       Cache["key"] = obj.processData();
  End Synclock
}

return Cache["key"];

Basically this stops you blocking on every call once the cache is hot and will yield better performance as well as protecting you more from potential race conditions.

基本上,一旦缓存变热,这会阻止您在每次通话时阻止,并且会产生更好的性能,同时保护您免受潜在竞争条件的影响。

Bear in mind, as soon as you have two different locks, you open yourself up to potential deadlocks (and that's far beyond the scope of this thread).

请记住,只要你有两个不同的锁,你就可以打开潜在的死锁(这远远超出了这个线程的范围)。

Look for a .Net caching solution.

寻找.Net缓存解决方案。

#1


Is it critical that it is never calculated twice? i.e. if two threads happened to ask for it at the same time, and calculate the value independently, is that a show-stopper? In most cases, it isn't - in which case, just check for null (since it is a string): (example in C#, apologies):

它永远不会被计算两次至关重要吗?即如果两个线程同时要求它,并独立计算该值,那么它是一个显示器吗?在大多数情况下,它不是 - 在这种情况下,只检查null(因为它是一个字符串):( C#中的例子,道歉):

   if(processedData == null) {
       processedData = DoStuff();
   }
   return processedData;

All subsequent calls should see the new value (I don't think we'll need volatile if it is hidden inside a property/method).

所有后续调用都应该看到新值(如果它隐藏在属性/方法中,我认为我们不需要volatile)。

This has the advantage of being lock-free and simple.

这具有无锁且简单的优点。

Another trick is to use a static property of a nested class:

另一个技巧是使用嵌套类的静态属性:

string SomeValue {
   get {return MyCache.SomeValue;}
}
static class MyCache {
    public static readonly string SomeValue;
    static MyCache() {
         SomeValue = DoStuff();
    }
}

This is calculated lazily, but the rules of static initializers mean that it is guaranteed to run once only (excluding reflection).

这是懒惰计算的,但静态初始化程序的规则意味着它只能运行一次(不包括反射)。

#2


You can improve the concurrency with a double-check optimization:

您可以通过双重检查优化来提高并发性:

If Not DataCached Then
    Synclock Lock
    If Not DataCached Then
        ProcessedData = DoStuff()
        DataCached = True ' Set this AFTER processing
    End If
End Synclock

This will avoid the critical section after the first init.

这将避免第一个init之后的关键部分。

#3


This is the only way, there could be some other system library to do this, but eventually that library also would do same thing internally.

这是唯一的方法,可能有一些其他系统库来做到这一点,但最终该库也会在内部做同样的事情。

#4


Firslty, I would move the caching outside of your class that contains the business logic and keep your business logic pure and allow you to control the caching independent of the application. But that's not your question ...

Firslty,我会在您的类之外移动包含业务逻辑的缓存,并保持您的业务逻辑纯净,并允许您独立于应用程序控制缓存。但那不是你的问题......

You need did not mention if you'll take more of a hit potentially calculating the things multiple times, until the cache is hot. The simple approach there would be:

你需要没有提到你是否会多次冒险,可能会多次计算这些东西,直到缓存很热。简单的方法是:

if (Cache["key"] == null)
  Cache["key"] = obj.processData();

return Cache["key"];

Cache itself should ensure that this is safe.

缓存本身应该确保这是安全的。

If you wish to explicitly block whilst the cache is being populated, then you already have the semantics to do so in your code above, however I recommend this change:

如果你希望在填充缓存时显式阻塞,那么你已经在上面的代码中有了这样做的语义,但是我推荐这个改变:

if (Cache["key"] == null) {
  Synclock blockIfProcessing
    if (Cache["key"] == null) 
       Cache["key"] = obj.processData();
  End Synclock
}

return Cache["key"];

Basically this stops you blocking on every call once the cache is hot and will yield better performance as well as protecting you more from potential race conditions.

基本上,一旦缓存变热,这会阻止您在每次通话时阻止,并且会产生更好的性能,同时保护您免受潜在竞争条件的影响。

Bear in mind, as soon as you have two different locks, you open yourself up to potential deadlocks (and that's far beyond the scope of this thread).

请记住,只要你有两个不同的锁,你就可以打开潜在的死锁(这远远超出了这个线程的范围)。

Look for a .Net caching solution.

寻找.Net缓存解决方案。