使缓存计算线程的结果安全的最具性能的方法是什么

本文关键字:性能 方法 是什么 结果 缓存 计算 线程 安全 | 更新日期: 2023-09-27 18:20:59

我有一个昂贵的计算,它的结果经常被访问,但很少更改。因此,我缓存得到的值。这里有一些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线程安全的最具性能的方法是什么?

很明显,我可以把整件事都锁起来(),但我怀疑可能有更好的方法?(它会进行原子检查,看看结果是否需要重新计算,如果需要,只锁定?)

非常感谢!

使缓存计算线程的结果安全的最具性能的方法是什么

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

// 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;
    }
}

这个特定的实现与您对它在极端情况下应该做什么的描述相匹配:如果缓存已经无效,那么单个线程只应该计算一次值,其他线程应该等待。但是,如果缓存与正在访问的缓存值同时无效,则可能会返回过时的缓存值。

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

  1. 泛型类
  2. 该类可以异步或同步计算数据
  3. 由于自旋锁,允许快速读取
  4. 不在spinlock中执行繁重的工作,只返回Task,如果需要,在默认的TaskScheduler上创建并启动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
            }
        }
    }

您可以简单地重新分配Lazy<T>以实现重置:

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