public void TestValueMatch() { var valueMatch = new ValueMatch(); Assert.IsTrue(valueMatch.IsMatch(_card1, _card3)); Assert.IsFalse(valueMatch.IsMatch(_card2, _card3)); }
internal abstract bool PutIfMatch(TKey key, object newVal, ref object oldValue, ValueMatch match);
internal abstract bool RemoveIfMatch(TKey key, ref TValue oldValue, ValueMatch match);
// 1) finds or creates a slot for the key // 2) sets the slot value to the putval if original value meets expVal condition // 3) returns true if the value was actually changed // Note that pre-existence of the slot is irrelevant // since slot without a value is as good as no slot at all internal sealed override bool PutIfMatch(TKey key, object newVal, ref object oldVal, ValueMatch match) { if (key == null) { throw new ArgumentNullException(nameof(key)); } var curTable = this; int fullHash = curTable.hash(key); TRY_WITH_NEW_TABLE: Debug.Assert(newVal != null); Debug.Assert(!(newVal is Prime)); var curEntries = curTable._entries; int lenMask = curEntries.Length - 1; int idx = ReduceHashToIndex(fullHash, lenMask); // Spin till we get a slot for the key or force a resizing. int reprobeCnt = 0; while (true) { // hash, key and value are all CAS-ed down and follow a specific sequence of states. // hence the order of their reads is irrelevant and they do not need to be volatile var entryHash = curEntries[idx].hash; if (entryHash == 0) { // Found an unassigned slot - which means this // key has never been in this table. if (newVal == TOMBSTONE) { Debug.Assert(match == ValueMatch.NotNullOrDead || match == ValueMatch.OldValue); oldVal = null; goto FAILED; } else { // Slot is completely clean, claim the hash first Debug.Assert(fullHash != 0); entryHash = Interlocked.CompareExchange(ref curEntries[idx].hash, fullHash, 0); if (entryHash == 0) { entryHash = fullHash; if (entryHash == ZEROHASH) { // "added" entry for zero key curTable.allocatedSlotCount.Increment(); break; } } } } if (entryHash == fullHash) { // hash is good, one way or another, // try claiming the slot for the key if (curTable.TryClaimSlotForPut(ref curEntries[idx].key, key)) { break; } } // here we know that this slot does not map to our key // and must reprobe or resize // hitting reprobe limit or finding TOMBPRIMEHASH here means that the key is not in this table, // but there could be more in the new table if (++reprobeCnt >= ReprobeLimit(lenMask) | entryHash == TOMBPRIMEHASH) { // start resize or get new table if resize is already in progress var newTable1 = curTable.Resize(); // help along an existing copy curTable.HelpCopy(); curTable = newTable1; goto TRY_WITH_NEW_TABLE; } curTable.ReprobeResizeCheck(reprobeCnt, lenMask); // quadratic reprobing idx = (idx + reprobeCnt) & lenMask; } // Found the proper Key slot, now update the Value. // We never put a null, so Value slots monotonically move from null to // not-null (deleted Values use Tombstone). // volatile read to make sure we read the element before we read the _newTable // that would guarantee that as long as _newTable == null, entryValue cannot be a Prime. var entryValue = Volatile.Read(ref curEntries[idx].value); // See if we want to move to a new table (to avoid high average re-probe counts). // We only check on the initial set of a Value from null to // not-null (i.e., once per key-insert). var newTable = curTable._newTable; // newTable == entryValue only when both are nulls if ((object)newTable == (object)entryValue && curTable.TableIsCrowded(lenMask)) { // Force the new table copy to start newTable = curTable.Resize(); Debug.Assert(curTable._newTable != null && newTable == curTable._newTable); } // See if we are moving to a new table. // If so, copy our slot and retry in the new table. if (newTable != null) { var newTable1 = curTable.CopySlotAndGetNewTable(idx, shouldHelp: true); Debug.Assert(newTable == newTable1); curTable = newTable; goto TRY_WITH_NEW_TABLE; } // We are finally prepared to update the existing table while (true) { Debug.Assert(!(entryValue is Prime)); var entryValueNullOrDead = EntryValueNullOrDead(entryValue); switch (match) { case ValueMatch.Any: if (newVal == entryValue) { // Do not update! goto FAILED; } break; case ValueMatch.NullOrDead: if (entryValueNullOrDead) { break; } oldVal = entryValue; goto FAILED; case ValueMatch.NotNullOrDead: if (entryValueNullOrDead) { goto FAILED; } break; case ValueMatch.OldValue: Debug.Assert(oldVal != null); if (!oldVal.Equals(entryValue)) { oldVal = entryValue; goto FAILED; } break; } // Actually change the Value var prev = Interlocked.CompareExchange(ref curEntries[idx].value, newVal, entryValue); if (prev == entryValue) { // CAS succeeded - we did the update! // Adjust sizes if (entryValueNullOrDead) { oldVal = null; if (newVal != TOMBSTONE) { curTable._size.Increment(); } } else { oldVal = prev; if (newVal == TOMBSTONE) { curTable._size.Decrement(); } } return(true); } // Else CAS failed // If a Prime'd value got installed, we need to re-run the put on the new table. if (prev is Prime) { curTable = curTable.CopySlotAndGetNewTable(idx, shouldHelp: true); goto TRY_WITH_NEW_TABLE; } // Otherwise we lost the CAS to another racing put. // Simply retry from the start. entryValue = prev; } FAILED: return(false); }
internal abstract bool PutIfMatch(TKey key, TValue newVal, ref TValue oldValue, ValueMatch match);