HopperCache.cs 10 KB

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