class TaskTracker
{
private volatile ConcurrentBag<Task> _bag = new();
public async Task WaitAllAvailableAsync()
{
await Task.WhenAll(Interlocked.Exchange(ref _bag, new()));
}
public void AddTask(Task toWait)
{
// should guarantee that the Task will be awaited on at least one WaitAllAvailableAsync call
ConcurrentBag<Task> oldBag, bag = _bag;
do
{
bag.Add(toWait);
oldBag = bag;
} while ((bag = _bag) != oldBag);
}
}
This class allows its user to add tasks to the internal list. When the WaitAllAvailableAsync is called, all currently added tasks are removed from the list and awaited. This may be used as a barrier mechanism to wait for all already started incoming requests to complete (without blocking adding newer requests). Both methods are thread safe and can be used from any number of threads. WaitAllAvailableAsync is called on a timer.
I'm not sure about the guarantees applied to this line:
} while ((bag = _bag) != oldBag);
Image that Interlocked.Exchange( have already executed at this point and another instance of bag was written. Does volatile read of _bag really guarantees seeing the new value? Would Interlocked.CompareExchange(ref _bag, null, null) provide better guarantees?
I saw many different opinions about the difference between Interlocked and volatile non-caching guarantees. Some people say that volaile only prevents reordering, others say that is also ensures reading somewhat fresh value but it can be stale by a few milli/nanoseconds. For the code above reading any stale value can lead to that the task will be added to an already discarded _bag instance and thus the task will never be awaited. I'm not sure whom to believe so I decided to ask here.
So are there any thread-safety issues here? Is there any scenario when added task will not be awaited inside WaitAllAvailableAsync call at least once (assuming endless running)?
I know that this code may be rewritten without volatile by using ConcurrentQueue but here I'm trying to understand specifically the behavior of the volatile read in this example.
In my opinion, no, there is no scenario where an added task will not be awaited by the
WaitAllAvailableAsyncat least once. The code as is should perform correctly its intended purpose, on all systems that are currently (.NET 8) supported by the .NET runtime.My understanding is that in practice (on all currently supported systems) reading a
volatilefield returns a value equally fresh with reading the same field withInterlocked.CompareExchange(ref _field, null, null). Which should return a value fresh enough to satisfy the requirements for the correctness of the code in the question. I would be very surprised if it was proven experimentally that a system exists where the code in the question fails to work correctly.