3

Sometimes I encounter async/await code that accesses fields of an object. For example this snippet of code from the Stateless project:

private readonly Queue<QueuedTrigger> _eventQueue = new Queue<QueuedTrigger>();
private bool _firing;

async Task InternalFireQueuedAsync(TTrigger trigger, params object[] args)
{
    if (_firing)
    {
        _eventQueue.Enqueue(new QueuedTrigger { Trigger = trigger, Args = args });
        return;
    }

    try
    {
        _firing = true;

        await InternalFireOneAsync(trigger, args).ConfigureAwait(false);

        while (_eventQueue.Count != 0)
        {
            var queuedEvent = _eventQueue.Dequeue();
            await InternalFireOneAsync(queuedEvent.Trigger, queuedEvent.Args).ConfigureAwait(false);
        }
    }
    finally
    {
        _firing = false;
    }
}

If I understand correctly the await **.ConfigureAwait(false) indicates that the code that is executed after this await does not necessarily has to be executed on the same context. So the while loop here could be executed on a ThreadPool thread. I don't see what is making sure that the _firing and _eventQueue fields are synchronized, for example what is creating the a lock/memory-fence/barrier here? So my question is; do I need to make the fields thread-safe, or is something in the async/await structure taking care of this?

Edit: to clarify my question; in this case InternalFireQueuedAsync should always be called on the same thread. In that case only the continuation could run on a different thread, which makes me wonder, do I need synchronization-mechanisms(like an explicit barrier) to make sure the values are synchronized to avoid the issue described here: http://www.albahari.com/threading/part4.aspx

Edit 2: there is also a small discussion at stateless: https://github.com/dotnet-state-machine/stateless/issues/294

Denxorz
  • 316
  • 1
  • 4
  • 15
  • 1
    `ConfigureAwait(false)` causes `while` to run on the same thread where the first `InternalFireOneAsync` was executed. You still need to do manual synchronization in multithreaded environment. This code is not thread-safe. – dymanoid Dec 06 '18 at 10:43
  • Whatever else may be true, `InternalFireQueuedAsync` is racy if called by multiple threads. If one thread is running the `while` loop, it may reach a point at which it is empty. It's therefore *just about to set `_firing` to `false` but hasn't yet*. If another thread enters at the time, it'll see the `_firing` is `true` and `Enqueue` the trigger. That trigger won't be executed until (at some later point in time) some other thread comes along with a new item to enqueue. – Damien_The_Unbeliever Dec 06 '18 at 11:25
  • @damien, indeed in the case of Stateless it is specified that you may only use it on a single thread, so there it should not be an issue. If that was not the case also the Enqueue and Dequeue should be protected, because the Queue class also not threadsafe. – Denxorz Dec 06 '18 at 11:29

4 Answers4

6

I don't see what is making sure that the _firing and _eventQueue fields are synchronized, for example what is creating the a lock/memory-fence/barrier here? So my question is; do I need to make the fields thread-safe, or is something in the async/await structure taking care of this?

await will ensure all necessary memory barriers are in place. However, that doesn't make them "thread-safe".

in this case InternalFireQueuedAsync should always be called on the same thread.

Then _firing is fine, and doesn't need volatile or anything like that.

However, the usage of _eventQueue is incorrect. Consider what happens when a thread pool thread has resumed the code after the await: it is entirely possible that Queue<T>.Count or Queue<T>.Dequeue() will be called by a thread pool thread at the same time Queue<T>.Enqueue is called by the main thread. This is not threadsafe.

If the main thread calling InternalFireQueuedAsync is a thread with a single-threaded context (such as a UI thread), then one simple fix is to remove all the instances of ConfigureAwait(false) in this method.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    _at the same time Queue.Enqueue is called by the main thread_ - this would only occur if the state machine is being used concurrently, correct? While the state machine design is explicitly non-concurrent. I could be missing something - thanks for posting your thoughts on this :-) – Nicholas Blumhardt Feb 14 '19 at 01:29
  • 1
    @NicholasBlumhardt: Yes, I mean if `InternalFireQueuedAsync` is called before the task returned from a previous call to `InternalFireQueuedAsync` has completed. – Stephen Cleary Feb 14 '19 at 03:43
  • @StephenCleary What ensures that `_firing` is fine? I believe `_firing` is possibly updated from many different threads (e.g., the thread calling `InternalFireQueuedAsync` and any threadpool thread running the continuations where the context is not captured). What ensures that `_firing` is not stale given that it's updated on multiple threads? – estebro Aug 31 '22 at 00:04
  • @StephenCleary Along the same lines of my previous comment. What guarantees the correctness of the `_eventQueue` instance after multiple iterations of the while loop, where each continuation may technically be executed on a different thread (given the context is not captured)? Is it possible for the `Dequeue` to not be properly reflected/observed across different threads (including the main thread which initially called `InternalFireQueuedAsync`)? If that is not possible, what guarantees this? – estebro Aug 31 '22 at 00:14
  • @estebro: The async/await compiler generated code emits a full memory barrier. – Stephen Cleary Aug 31 '22 at 00:55
  • @StephenCleary Thanks for the response. Is that documented anywhere? I expected as much, otherwise async/await would not be very useful, but I am having a hard time finding any documentation stating as much. – estebro Sep 01 '22 at 01:33
  • @estebro: No, I don't believe so. But `await` would be a huge pit of failure without a barrier or equivalent guarantee. – Stephen Cleary Sep 01 '22 at 02:30
0

To be safe, you should mark field _firing as volatile - that will guarantee the memory barrier and be sure that the continuation part, which might run on a different thread, will read the correct value. Without volatile, the compiler, the CLR or the JIT compiler, or even the CPU may do some optimizations that cause the code to read a wrong value for it.

As for _eventQueue, you don't modify the field, so marking it as volatile is useless. If only one thread calls 'InternalFireQueuedAsync', you don't access it from multiple threads concurrently, so you are ok.

However, if multiple threads call InternalFireQueuedAsync, you will need to use a ConcurrentQueue instead, or lock your access to _eventQueue. You then better also lock your access to _firing, or access it using Interlocked, or replace it with a ManualResetEvent.

Nick
  • 4,787
  • 2
  • 18
  • 24
  • You are saying "To be safe", does that mean there is no memory barrier triggered by the await? Because then you could run into the issue that the value is never synchronized, as described here: http://www.albahari.com/threading/part4.aspx – Denxorz Dec 06 '18 at 11:47
  • Your source says: _Asynchronous callbacks that use the thread pool — these include asynchronous delegates, APM callbacks, and Task continuations_. I, however, couldn't find a proof for this, even in [this writeup](https://msdn.microsoft.com/en-us/magazine/jj863136.aspx). That is why I recommended `volatile`. – Nick Dec 06 '18 at 11:54
  • Stephen Cleary states this in one of his [examples](https://blog.stephencleary.com/2012/02/async-and-await.html): "DownloadFileAsync also started in the UI context, but then stepped out of its context by calling ConfigureAwait(false). The rest of DownloadFileAsync runs in the thread pool context. " – Denxorz Dec 06 '18 at 12:12
  • Sorry. I do know that the callbacks use the threadpool. What I had in mind is that I couldn't find a proof that **the callbacks on the threadpool are protected by a memory barrier**. – Nick Dec 06 '18 at 14:19
  • Me neither. So in that case I would always take the safe side and make the fields threadsafe. – Denxorz Dec 06 '18 at 14:52
0

ConfigureAwait(false) means that the Context is not captured to run the continuation. Using the Thread Pool Context does not mean that continuations are run in parallel. Using await before and within the while loop ensures that the code (continuations) are run sequentially so no need to lock in this case. You may have however a race condition when checking the _firing value.

polkduran
  • 2,533
  • 24
  • 34
-1

use lock or ConcurrentQueue.

solution with lock:

private readonly Queue<QueuedTrigger> _eventQueue = new Queue<QueuedTrigger>();
private bool _firing;
private object _eventQueueLock = new object();

async Task InternalFireQueuedAsync(TTrigger trigger, params object[] args)
{
if (_firing)
{
    lock(_eventQueueLock)
       _eventQueue.Enqueue(new QueuedTrigger { Trigger = trigger, Args = args });
    return;
}

try
{
    _firing = true;

    await InternalFireOneAsync(trigger, args).ConfigureAwait(false);

    lock(_eventQueueLock)
    while (_eventQueue.Count != 0)
    {
        var queuedEvent = _eventQueue.Dequeue();
        await InternalFireOneAsync(queuedEvent.Trigger, queuedEvent.Args).ConfigureAwait(false);
    }
}


finally
{
    _firing = false;
}

}

solution with ConcurrentQueue:

private readonly ConccurentQueue<QueuedTrigger> _eventQueue = new ConccurentQueue<QueuedTrigger>();
private bool _firing;

async Task InternalFireQueuedAsync(TTrigger trigger, params object[] args)
{
if (_firing)
{
    _eventQueue.Enqueue(new QueuedTrigger { Trigger = trigger, Args = args });
    return;
}

try
{
    _firing = true;

    await InternalFireOneAsync(trigger, args).ConfigureAwait(false);

    lock(_eventQueueLock)
    while (_eventQueue.Count != 0)
    {
        object queuedEvent; // change object > expected type
        if(!_eventQueue.TryDequeue())
           continue;
        await InternalFireOneAsync(queuedEvent.Trigger, queuedEvent.Args).ConfigureAwait(false);
    }
}


finally
{
    _firing = false;
}

}

notesjor
  • 19
  • 1
  • 3
    You can't await inside the block of a lock statement: https://blog.cdemi.io/async-waiting-inside-c-sharp-locks/ – kwitee Nov 01 '19 at 07:48