C# 高性能缓存

C# 高性能缓存,c#,.net,multithreading,C#,.net,Multithreading,下面的代码应该缓存最后一次读取。LastValueCache是一个可以被多个线程访问的缓存(这就是我使用共享内存的原因)。对我来说,有竞争条件是可以的,但我希望其他线程能够看到更改的LastValueCache class Repository { public Item LastValueCache { get { Thread.MemoryBarrier(); SomeType result =

下面的代码应该缓存最后一次读取。
LastValueCache
是一个可以被多个线程访问的缓存(这就是我使用共享内存的原因)。对我来说,有竞争条件是可以的,但我希望其他线程能够看到更改的
LastValueCache

class Repository
{
    public Item LastValueCache
    {
        get
        {
            Thread.MemoryBarrier();
            SomeType result = field;
            Thread.MemoryBarrier();
            return result;
        }
        set
        {
            Thread.MemoryBarrier();
            field = value;
            Thread.MemoryBarrier();
        }
    }

    public void Save(Item item)
    {
        SaveToDatabase(item);
        Item cached = LastValueCache;
        if (cached == null || item.Stamp > cached.Stamp)
        {
            LastValueCache = item;
        }
    }

    public void Remove(Timestamp stamp)
    {
        RemoveFromDatabase(item);
        Item cached = LastValueCache;
        if (cached != null && cached.Stamp == item.Stamp)
        {
            LastValueCache = null;
        }
    }

    public Item Get(Timestamp stamp)
    {
        Item cached = LastValueCache;
        if (cached != null && cached.Stamp == stamp)
        {
            return cached;
        }

        return GetFromDatabase(stamp);
    }
}

Repository
对象被许多线程使用。我不想使用锁定,因为它会影响性能,在我的情况下,性能比数据一致性更重要。问题是哪种最小的同步机制可以满足我的需要?可能
volatile
或者
get
set
中的单个
内存载体
就足够了?

volatile
应该足够了,并且对于您的情况来说性能最好。在一些潜在的情况下,
volatile
本身不足以确保读取获得最新的值,但在这种情况下,我认为这不是一个重要因素。有关详细信息,请参阅的“volatile关键字”部分。

替代用途可能是
ReaderWriterLockSlim
类,以便为您这样的读写器场景启用线程同步

    ReaderWriterLockSlim _rw = new ReaderWriterLockSlim();
    get
    {
        _rw.EnterReadLock();
        SomeType result = field;
        _rw.ExitReadLock();
        return result;
    }
    set
    {
        _rw.EnterWriteLock();
        field = value;
        _rw.ExitWriteLock();
    }
这是读写器同步的有效实现。Eric Lippert的这张照片可能会让你感兴趣

另一个有趣的选项可能是
ConcurrentExclusiveSchedulerPair
类,该类为任务创建两个TaskScheduler,可将其拆分为“具有读访问权限的任务”(并发)和“具有写访问权限的任务”(独占)


有关这方面的更多信息可以找到。

如果这是愚蠢的,你不需要投票否决我。
只要告诉我,我就会删除。
但我并没有遵循这个逻辑

public void Save(Item item)
{
    SaveToDatabase(item);
    Item cached = LastValueCache;
    if (cached == null || item.Stamp > cached.Stamp)
    {
        LastValueCache = item;
    }
}
您担心的是内存毫秒,但在更新缓存之前,您正在等待对数据库的写入。
基于公共项的Get stamp是一个密钥

假设db写入为20毫秒
db读数为10毫秒
缓存获取和缓存设置各为2毫秒

公共作废保存(项目)
SaveToDatabase(项目);20毫秒
项目缓存=LastValueCache;2毫秒
如果(cached==null | | item.Stamp>cached.Stamp)1毫秒
LastValueCache=项目;2毫秒

在LastValueCache=项目之前的23毫秒内;对公共项Get(Timestamp stamp)的任何调用都将命中数据库而不是缓存

在LastValueCache=项目之前的23毫秒内;对公共项LastValueCache的任何调用 get将获得一个过期时间长达23毫秒的值。 声明的目标是让其他线程看到LastValueCache,但它们看到的是过时的LastValueCache

“删除”也一样。
您将对本可以避免的数据库进行多次访问

你想达到什么目标?
你分析过这个吗

我打赌瓶颈在于对数据库的调用。
数据库调用比锁和内存载体之间的差异长1000倍

public void Save(Item item)
{   
   // add logic that the prior asynchonous call to SaveToDatabase is complete
   // if not wait for it to complete 
   // LastValueCache will possible be replaced so you need last item in the database 
   // the time for a lock is not really a factor as it will be faster than the prior update 
   Item cached = LastValueCache;
   if (cached == null || item.Stamp > cached.Stamp)
   {
       LastValueCache = item;
   }
   // make the next a task or background so it does not block    
   SaveToDatabase(item);
}
如果设置LastValueCache=item,甚至可以将逻辑更改为仅等待先前的调用
但是您需要以某种方式限制数据库

下一步是缓存最后一个X,并在Item Get(Timestamp stamp)中使用它
数据库是您需要优化的调用
同样,您需要配置文件


之后,逻辑会变得更复杂,但会将数据库调用提供给BlockingCollection。需要确保最后一个X缓存大于BlockingCollections大小。如果不阻塞,等待BC清除。您需要将相同的BC用于insert和delete,以便按顺序处理它们。可以变得足够聪明,只需不插入包含删除的记录。不要一次插入或删除一条记录

我实现了一个为并发工作负载设计的线程安全伪LRU:ConcurrentLru。性能非常接近ConcurrentDictionary,比MemoryCache快约10倍,命中率比传统LRU高。下面的github链接提供了完整的分析

用法如下所示:

int capacity = 666;
var lru = new ConcurrentLru<int, SomeItem>(capacity);

var value = lru.GetOrAdd(1, (k) => new SomeItem(k));

注意,如果没有竞争,锁是一个非常便宜的原语。你的关键部分很小(一打左右的说明)。这将花费大约2个联锁操作。;除此之外,我认为volatile应该起作用。但我不是这方面的专家。你现在使用的双重记忆屏障也应该有效。我不理解你的要求。如果竞争条件正常,则LastValueCache返回的值很容易无效。例如,有人对当前值调用remove,remove看到比当前值旧的缓存值,没有将缓存设置为null,然后boom,LastCacheValue引用的是已删除的项。这对我来说是可以的。但我不希望这种状态永远持续下去。也就是说,当我最终设置
LastValueCache
时,我希望其他线程可以刷新其引用。@Pellared:但这将是永远的(至少,直到下一次使用较新的时间戳进行保存,假设您的时间戳一直在增加)。@Cameron很遗憾,现在我不理解您的意思。时间如下:线程1-
RemoveFromDatabase
,线程2-
Get
(从缓存中获取删除的值),线程1-
LastValueCache=null
;线程2-
Get
(现在它从数据库中获取值)。这会慢得多。分配引用是原子的,因此锁定属性只会给我们带来开销。此外,正如我在问题中已经提到的,我不想要任何锁定。以前的实现在方法(ReadLock用于Get,WriteLock用于Save和Delete)周围使用了
readerwriterlocksim
,但是速度太慢了。你读过Eric Lippert的文章吗?但如果你认为你的代码是线程安全的,那就没问题了……是的,我是这么认为的。此外,他还回答了我关于它的另一个问题:我更喜欢锁,但是如果你真的需要锁,我会给你锁上
Install-Package BitFaster.Caching