【问题标题】:What is the most performant way to make the results of a cached computation thread-safe?使缓存计算的结果成为线程安全的最高效方法是什么?
【发布时间】:2015-11-07 14:31:55
【问题描述】:

(抱歉,如果在其他地方回答了这个问题;这似乎是一个常见问题,但事实证明很难搜索,因为“线程”和“缓存”等术语会产生压倒性的结果。)

我有一个昂贵的计算,其结果被频繁访问但很少更改。因此,我缓存了结果值。这是我的意思的一些 c# 伪代码:

int? _cachedResult = null;

int GetComputationResult()
{
    if(_cachedResult == null)
    {
        // Do the expensive computation.
        _cachedResult = /* Result of expensive computation. */;
    }
    return _cachedResult.Value;
}

在我的代码的其他地方,我偶尔会将 _cachedResult 设置回 null,因为计算的输入已更改,因此缓存的结果不再有效,需要重新计算。 (这意味着我不能使用Lazy<T>,因为Lazy<T> 不支持被重置。)

这适用于单线程场景,但当然它根本不是线程安全的。所以我的问题是:使GetComputationResult 线程安全的最高效方法是什么?

显然我可以把整个东西放在一个 lock() 块中,但我怀疑可能有更好的方法? (可以进行原子检查以查看是否需要重新计算结果并仅在需要时才锁定的东西?)

非常感谢!

【问题讨论】:

  • 线程偶尔获取陈旧值有多糟糕?
  • 当一个线程正在计算而另一个线程想要得到结果时,你期望会发生什么?它应该等待还是获取旧值?
  • 计算大概需要多少时间?
  • Lazy<T> 无法重置,但您可以改用LazyInitializer.EnsureInitialized。当您希望它再次初始化时,只需将其设置为 null 即可。尽管您必须在此处使用包装器引用类型而不是可为空的。
  • 只是,仅供参考,LazyInitializer 可能会多次调用初始化程序,但字段中会存储一个值。

标签: c# .net multithreading


【解决方案1】:

您可以使用双重检查锁定模式:

// Thread-safe (uses double-checked locking pattern for performance)
public class Memoized<T>
{
    Func<T> _compute;
    volatile bool _cached;
    volatile bool _startedCaching;
    volatile StrongBox<T> _cachedResult;  // Need reference type
    object _cacheSyncRoot = new object();

    public Memoized(Func<T> compute)
    {
        _compute = compute;
    }

    public T Value {
        get {
            if (_cached)    // Fast path
                return _cachedResult.Value;
            lock (_cacheSyncRoot)
            {
                if (!_cached)
                {
                    _startedCaching = true;
                    _cachedResult = new StrongBox<T>(_compute());
                    _cached = true;
                }
            }
            return _cachedResult.Value;
        }
    }

    public void Invalidate()
    {
        if (!_startedCaching)
        {
            // Fast path: already invalidated
            Thread.MemoryBarrier();  // need to release
            if (!_startedCaching)
                return;
        }
        lock (_cacheSyncRoot)
            _cached = _startedCaching = false;
    }
}

此特定实现符合您对在极端情况下应执行的操作的描述:如果缓存已失效,则该值应仅由单个线程计算一次,而其他线程应等待。但是,如果缓存在被访问的缓存值同时失效,则可能会返回陈旧的缓存值。

【讨论】:

  • 我认为_cachedResult 必须是易变的,并且您不能重新分配它的成员,因为重新分配不是原子的。需要一个新的实例。
  • 并且第一个 if 和 return 不是原子的组合。可能会返回一个陈旧的值。
  • @usr: _cachedResult 包含一个 volatile 字段。它对于大多数内置类型都是原子的,但不是所有的值类型。修复,谢谢!在争用下返回陈旧值的可能性是故意的,请参阅我的更新。
  • 谢谢,这正是我正在寻找的解决方案!
  • @hvd:啊,但是调用Invalidate() 的线程可能已经改变了一些影响新计算结果的状态。如果该值与Invalidate() 并行计算,那么我们希望在完成对Invalidate() 的调用之后 获得的值始终基于新状态。这让我意识到我错过了记忆障碍。正在编辑...
【解决方案2】:

也许这会提供一些思考的食物:)。

  1. 通用类。
  2. 该类可以异步或同步计算数据。
  3. 借助自旋锁实现快速读取。
  4. 不在自旋锁内执行繁重的工作,只返回 Task,如有必要,在默认 TaskScheduler 上创建和启动 Task,以避免内联。

Task 与 Spinlock 的组合非常强大,可以用无锁的方式解决一些问题。

    using System;
    using System.Threading;
    using System.Threading.Tasks;

    namespace Example
    {
        class OftenReadSometimesUpdate<T>
        {
            private Task<T> result_task = null;
            private SpinLock spin_lock = new SpinLock(false);
            private TResult LockedFunc<TResult>(Func<TResult> locked_func)
            {
                TResult t_result = default(TResult);
                bool gotLock = false;
                if (locked_func == null) return t_result;

                try
                {
                    spin_lock.Enter(ref gotLock);
                    t_result = locked_func();
                }
                finally
                {
                    if (gotLock) spin_lock.Exit();
                    gotLock = false;
                }
                return t_result;
            }


            public Task<T> GetComputationAsync()
            {
                return
                LockedFunc(GetComputationTaskLocked)
                ;
            }
            public T GetComputationResult()
            {
                return
                LockedFunc(GetComputationTaskLocked)
                .Result
                ;
            }
            public OftenReadSometimesUpdate<T> InvalidateComputationResult()
            {
                return
                this
                .LockedFunc(InvalidateComputationResultLocked)
                ;
            }
            public OftenReadSometimesUpdate<T> InvalidateComputationResultLocked()
            {
                result_task = null;
                return this;
            }

            private Task<T> GetComputationTaskLocked()
            {
                if (result_task == null)
                {
                    result_task = new Task<T>(HeavyComputation);
                    result_task.Start(TaskScheduler.Default);
                }
                return result_task;
            }
            protected virtual T HeavyComputation()
            {
                //a heavy computation
                return default(T);//return some result of computation
            }
        }
    }

【讨论】:

    【解决方案3】:

    您可以简单地重新分配Lazy&lt;T&gt; 来实现重置:

    Lazy<int> lazyResult = new Lazy<int>(GetComputationResult);
    
    public int Result { get { return lazyResult.Value; } }
    
    public void Reset()
    {
       lazyResult = new Lazy<int>(GetComputationResult);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-09-09
      • 1970-01-01
      • 2013-03-17
      • 2012-08-14
      • 1970-01-01
      • 2015-11-29
      • 1970-01-01
      相关资源
      最近更新 更多