【问题标题】:Thread-safe one time calculation best practices线程安全的一次性计算最佳实践
【发布时间】:2010-10-17 15:34:10
【问题描述】:

在我的类中需要一个需要计算和缓存的属性是很常见的。

通常我使用一个锁和一个布尔值来检查它是否被处理。有时我会在访问器中这样做。

这种方法对性能有何影响?有没有更好的方法。

我对此的常用方法的示例代码:

   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

编辑:

这是访问时需要计算的东西,因为它一直在变化。构造函数计算在这里没有帮助。 (示例是我所做工作的真正简化版本

【问题讨论】:

    标签: .net multithreading caching locking


    【解决方案1】:

    从不计算两次是否很重要?即如果两个线程发生同时要求它,并独立计算值,那是一个显示停止器吗?在大多数情况下,它不是 - 在这种情况下,只需检查 null(因为它是一个字符串):(C# 中的示例,道歉):

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

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

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

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

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

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

    【讨论】:

    • 这其实是个好点,最坏的情况下会计算3次。我敢肯定不止这些。
    • (请参阅更新以获取在不使用锁的情况下永远不会重新计算它的方法)
    • @Marc 即使没有锁我仍然认为内部有锁,对吧?
    • 老实说,我不知道它是如何在幕后实现的——不过是类似的。
    【解决方案2】:

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

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

    这将避免第一次初始化后的临界区。

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

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

        在缓存变热之前,您无需提及是否会多次计算可能会受到更多影响。简单的方法是:

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

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

        如果您希望在填充缓存时显式阻止,那么您在上面的代码中已经有了这样做的语义,但是我建议进行此更改:

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

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

        请记住,一旦您拥有两个不同的锁,您就会面临潜在的死锁(这远远超出了本主题的范围)。

        寻找 .Net 缓存解决方案。

        【讨论】:

        • 理论上这可能会返回 null (另一个线程可以在您分配它和返回它之间从缓存中删除该项目。为了避免这种情况,请使用类似 temp = Cache["key"]; if (temp == null) { temp = processData(); Cache["key" = temp; } return temp;
        猜你喜欢
        • 2019-12-31
        • 2014-08-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-11-29
        • 2010-09-05
        • 2010-09-28
        相关资源
        最近更新 更多