1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
|
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------
namespace System.Runtime.Collections
{
using System;
using System.Collections;
using System.Threading;
using System.Diagnostics;
// This cache works like a MruCache, but operates loosely and without locks in the mainline path.
//
// It consists of three 'hoppers', which are Hashtables (chosen for their nice threading characteristics - reading
// doesn't require a lock). Items enter the cache in the second hopper. On lookups, cache hits result in the
// cache entry being promoted to the first hopper. When the first hopper is full, the third hopper is dropped,
// and the first and second hoppers are shifted down, leaving an empty first hopper. If the second hopper is
// full when a new cache entry is added, the third hopper is dropped, the second hopper is shifted down, and a
// new second hopper is slotted in to become the new item entrypoint.
//
// Items can only be added and looked up. There's no way to remove an item besides through attrition.
//
// This cache has a built-in concept of weakly-referenced items (which can be enabled or disabled in the
// constructor). It needs this concept since the caller of the cache can't remove dead cache items itself.
// A weak HopperCache will simply ignore dead entries.
//
// This structure allows cache lookups to be almost lock-free. The only time the first hopper is written to
// is when a cache entry is promoted. Promoting a cache entry is not critical - it's ok to skip a promotion.
// Only one promotion is allowed at a time. If a second is attempted, it is skipped. This allows promotions
// to be synchronized with just an Interlocked call.
//
// New cache entries go into the second hopper, which requires a lock, as does shifting the hoppers down.
//
// The hopperSize parameter determines the size of the first hopper. When it reaches this size, the hoppers
// are shifted. The second hopper is allowed to grow to twice this size. This is because it needs room to get
// new cache entries into the system, and the second hopper typically starts out 'full'. Entries are never added
// directly to the third hopper.
//
// It's a error on the part of the caller to add the same key to the cache again if it's already in the cache
// with a different value. The new value will not necessarily overwrite the old value.
//
// If a cache entry is about to be promoted from the third hopper, and in the mean time the third hopper has been
// shifted away, an intervening GetValue for the same key might return null, even though the item is still in
// the cache and a later GetValue might find it. So it's very important never to add the same key to the cache
// with two different values, even if GetValue returns null for the key in-between the first add and the second.
// (If this particular behavior is a problem, it may be possible to tighten up, but it's not necessary for the
// current use of HopperCache - UriPrefixTable.)
class HopperCache
{
readonly int hopperSize;
readonly bool weak;
Hashtable outstandingHopper;
Hashtable strongHopper;
Hashtable limitedHopper;
int promoting;
LastHolder mruEntry;
public HopperCache(int hopperSize, bool weak)
{
Fx.Assert(hopperSize > 0, "HopperCache hopperSize must be positive.");
this.hopperSize = hopperSize;
this.weak = weak;
this.outstandingHopper = new Hashtable(hopperSize * 2);
this.strongHopper = new Hashtable(hopperSize * 2);
this.limitedHopper = new Hashtable(hopperSize * 2);
}
// Calls to Add must be synchronized.
public void Add(object key, object value)
{
Fx.Assert(key != null, "HopperCache key cannot be null.");
Fx.Assert(value != null, "HopperCache value cannot be null.");
// Special-case DBNull since it can never be collected.
if (this.weak && !object.ReferenceEquals(value, DBNull.Value))
{
value = new WeakReference(value);
}
Fx.Assert(this.strongHopper.Count <= this.hopperSize * 2,
"HopperCache strongHopper is bigger than it's allowed to get.");
if (this.strongHopper.Count >= this.hopperSize * 2)
{
Hashtable recycled = this.limitedHopper;
recycled.Clear();
recycled.Add(key, value);
// The try/finally is here to make sure these happen without interruption.
try { } finally
{
this.limitedHopper = this.strongHopper;
this.strongHopper = recycled;
}
}
else
{
// We do nothing to prevent things from getting added multiple times. Also may be writing over
// a dead weak entry.
this.strongHopper[key] = value;
}
}
// Calls to GetValue do not need to be synchronized, but the object used to synchronize the Add calls
// must be passed in. It's sometimes used.
public object GetValue(object syncObject, object key)
{
Fx.Assert(key != null, "Can't look up a null key.");
WeakReference weakRef;
object value;
// The MruCache does this so we have to too.
LastHolder last = this.mruEntry;
if (last != null && key.Equals(last.Key))
{
if (this.weak && (weakRef = last.Value as WeakReference) != null)
{
value = weakRef.Target;
if (value != null)
{
return value;
}
this.mruEntry = null;
}
else
{
return last.Value;
}
}
// Try the first hopper.
object origValue = this.outstandingHopper[key];
value = this.weak && (weakRef = origValue as WeakReference) != null ? weakRef.Target : origValue;
if (value != null)
{
this.mruEntry = new LastHolder(key, origValue);
return value;
}
// Try the subsequent hoppers.
origValue = this.strongHopper[key];
value = this.weak && (weakRef = origValue as WeakReference) != null ? weakRef.Target : origValue;
if (value == null)
{
origValue = this.limitedHopper[key];
value = this.weak && (weakRef = origValue as WeakReference) != null ? weakRef.Target : origValue;
if (value == null)
{
// Still no value? It's not here.
return null;
}
}
this.mruEntry = new LastHolder(key, origValue);
// If we can get the promoting semaphore, move up to the outstanding hopper.
int wasPromoting = 1;
try
{
try { } finally
{
// This is effectively a lock, which is why it uses lock semantics. If the Interlocked call
// were 'lost', the cache wouldn't deadlock, but it would be permanently broken.
wasPromoting = Interlocked.CompareExchange(ref this.promoting, 1, 0);
}
// Only one thread can be inside this 'if' at a time.
if (wasPromoting == 0)
{
Fx.Assert(this.outstandingHopper.Count <= this.hopperSize,
"HopperCache outstandingHopper is bigger than it's allowed to get.");
if (this.outstandingHopper.Count >= this.hopperSize)
{
lock (syncObject)
{
Hashtable recycled = this.limitedHopper;
recycled.Clear();
recycled.Add(key, origValue);
// The try/finally is here to make sure these happen without interruption.
try { } finally
{
this.limitedHopper = this.strongHopper;
this.strongHopper = this.outstandingHopper;
this.outstandingHopper = recycled;
}
}
}
else
{
// It's easy for this to happen twice with the same key.
//
// It's important that no one else can be shifting the current oustandingHopper
// during this operation. We are only allowed to modify the *current* outstandingHopper
// while holding the pseudo-lock, which would be violated if it could be shifted out from
// under us (and potentially added to by Add in a ----).
this.outstandingHopper[key] = origValue;
}
}
}
finally
{
if (wasPromoting == 0)
{
this.promoting = 0;
}
}
return value;
}
class LastHolder
{
readonly object key;
readonly object value;
internal LastHolder(object key, object value)
{
this.key = key;
this.value = value;
}
internal object Key
{
get
{
return this.key;
}
}
internal object Value
{
get
{
return this.value;
}
}
}
}
}
|