פתרון סנכרון ומצב משותף¶
ה־CAS משווה את הערך שנקרא לערך הנוכחי לפני שינוי. במקרה שהשוואה נכשלת ננסה שוב עם מידע חדש; לא נשמור את תוצאת הבדיקה הקודמת.
var budget = new Budget(3);
Task[] tasks = Enumerable.Range(0, 8).Select(_ => Task.Run(() =>
{
for (int i = 0; i < 1_000; i++)
{
if (!budget.TryAcquire()) continue;
try { double ignored = Math.Sqrt(i); }
finally { budget.Release(); }
}
})).ToArray();
await Task.WhenAll(tasks);
Console.WriteLine(budget.Available);
sealed class Budget(int capacity)
{
private readonly int capacity = capacity > 0 ? capacity : throw new ArgumentOutOfRangeException();
private int available = capacity;
public int Available => Volatile.Read(ref available);
public bool TryAcquire()
{
while (true)
{
int old = Volatile.Read(ref available);
if (old == 0) return false;
if (Interlocked.CompareExchange(ref available, old - 1, old) == old) return true;
}
}
public void Release()
{
while (true)
{
int old = Volatile.Read(ref available);
if (old == capacity) throw new InvalidOperationException("over-release");
if (Interlocked.CompareExchange(ref available, old + 1, old) == old) return;
}
}
}
בגרסת lock נחזיק gate פרטי ונבצע כל בדיקה ועדכון בתוך אותו בלוק, לרבות Available אם רוצים snapshot מסונכרן. ההוכחה פשוטה יותר ויכולה להיות הבחירה הנכונה גם אם גרסת CAS נראית קצרה. שתי הגרסאות מנהלות ספירה בלבד; זיהוי שחרור כפול של lease מסוים דורש אובייקט בעלות ומצב שחרור חד־פעמי.
ה־Snapshot הלא־נעול יכול לקרוא count אחרי שה־writer כתב אותו, ואז לקרוא bytes לפני עדכונו. אפילו אם במכונה מסוימת קשה לשחזר את החלון, אין סנכרון שמחייב את הקורא לראות את הפעולה הלוגית בשלמותה.