6

I have a service that ensures that exactly one popup is displayed at the same time. AddPopupAsync can be called concurrently, i.e. a popup is open while another 10 AddPopupAsync requests drop in. The code:

public async Task<int> AddPopupAsync(Message message)
{
    //[...]

    while (popupQueue.Count != 0)
    {
        await popupQueue.Peek();
    }

    return await Popup(interaction);
}

But I can spot two unwanted things that can happen because of the lack of thread safety:

  1. If the queue is empty, Peek will throw an exception
  2. If a thread A is preempted before the first statement in Popup, another thread B will not wait for the pending popup A since the queue is still empty.

Popup method works with a TaskCompletionSource and before calling its SetResult method, popupQueue.Dequeue() is called.

I think about using the atomic TryPeek from ConcurrentQueue in order to make #1 thread safe:

do
{
    Task<int> result;

    bool success = popupQueue.TryPeek(out result);

    if (!success) break;
    await result;
} 
while (true);

Then I read about a AsyncProducerConsumerCollection but I'm not sure if that is the most simple solution.

How can I ensure thread safety in a simple way? Thank you.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
user4287276
  • 61
  • 1
  • 3
  • 1
    I think you've answered your own question. `ConcurrentQueue` has what you need built-in. – Yuval Itzchakov Nov 24 '14 at 12:42
  • Your current semantics are that all calls to `AddPopupAsync` will not complete until all popups are shown (not just the one that that particular call is showing). Are you sure that's the semantics you want? – Stephen Cleary Nov 24 '14 at 12:51
  • @Stephen Cleary Since PopupView and PopupViewModel are singletons, the state of the modal popup window is changed whenever Popup is called. And that is why I'm using a queue. – user4287276 Nov 24 '14 at 13:12
  • 1
    Wouldn't it make sense to treat this UI-service-like-thing as though it were part of the UI; i.e., only call `AddPopupAsync` from the UI context? – Stephen Cleary Nov 24 '14 at 13:14
  • @Stephen Cleary: If AddPopupAsync is only called from the UI thread, AddPopupAsync runs on a worker thread because of the await keyword. Right? Do you want to say that there is no need for asynchronous functions since there is no compute-bound work in AddPopupAsync and if there aren't asynchronous functions any longer, the code will be thread safe? – user4287276 Nov 24 '14 at 13:52
  • `await` does not introduce worker threads. Unless you're calling `AddPopupAsync` from *other threads*, it doesn't need to be threadsafe. For more about how `async` works, check out my [`async` intro](http://blog.stephencleary.com/2012/02/async-and-await.html). – Stephen Cleary Nov 24 '14 at 14:01
  • Just FYI: Its up to the AddPopupAsync how async is implemented or implemented at all, see http://stackoverflow.com/questions/27051169/why-does-await-loadasync-freeze-the-ui-while-await-task-run-load/27071434#27071434 – Rico Suter Nov 27 '14 at 10:17

1 Answers1

8

To simply add thread-safety you should use a ConcurrentQueue. It's a thread-safe implementation of a queue and you can use it just the same way you would use a queue without worrying about concurrency.

However if you want a queue you can await asynchronously (which means that you're not busy-waiting or blocking a thread while waiting) you should use TPL Dataflow's BufferBlock which is very similar to AsyncProducerConsumerCollection only already implemented for you by MS:

var buffer = new BufferBlock<int>();
await buffer.SendAsync(3); // Instead of enqueue.
int item = await buffer.ReceiveAsync(); // instead of dequeue.

ConcurrentQueue is fine for thread-safeness, but wasteful in high levels of contention. BufferBlock is not only thread-safe it also gives you asynchronous coordination (usually between a consumer and producer).

i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • But ConcurrentQueue doesn't indicate that the code using ConcurrentQueue is thread safe, does it (see #2 in my question)? – user4287276 Nov 24 '14 at 13:11
  • @user4287276 `ConcurrentQueue` makes sure each call to one of its methods is thread-safe by itself. It's not a locking mechanism. If you need synchronization in your code you should use a `lock` (or `AsyncLock` if its appropriate). – i3arnon Nov 24 '14 at 13:27
  • BufferBlock doesn't have a peek method – Arnold Pistorius May 16 '18 at 15:06
  • 1
    @ArnoldPistorius does it have a `TryReceive` method that accepts a predicate that can be used as a "peek"? – i3arnon May 16 '18 at 20:55