1

I have two asynchronous methods (let's call them AAsync() and BAsync()) that are intended to run one after the other one. This is because method AAsync() prepares the context for method BAsync() to act properly. For each call of AAsync() there's a correspondent call of the method BAsync().

These methods can be fired in the following order (as an example):

A1A2B1B2A3B3B4A4, where A stands for AAsync, B for BAsync, and the subindex indicates the call number of the method.

Given that it will always be that method AAsynci will be called after method AAsynci-1 and before AAsynci+1 (same applies for BAsync), and that a call AAsynci has a correspondant call BAsynci, I would like to know which is the best way to, given a disordered firing of the methods (like the one in the example above), execute the calls in the following order:

A1B1A2B2A3B3A4B4 ...

I have solved this using two FIFO Semaphores and two TaskCompletionSource (see below), but I have the feeling that this can be solved in a smarter way. Does anybody know a nicer solution?

TaskCompletionSource TCS_A;
TaskCompletionSource TCS_B;

FIFOSemaphore MutexA = new FIFOSemaphore(1);
FIFOSemaphore MutexB = new FIFOSemaphore(1);

async void AAsync()
{
    await MutexA.WaitAsync();

    if (TCS_B!= null)
        await TCS_B.Task;

    TCS_B = new TaskCompletionSource<bool>();

    // Do intended stuff...

    TCS_A?.TrySetResult(true);

    MutexA.Release();
}

async void BAsync()
{
    await MutexB.WaitAsync();

    if (TCS_A!= null)
        await TCS_A.Task;
            
    TCS_A = new TaskCompletionSource<bool>();

    // Do intended stuff...

    TCS_B?.TrySetResult(true);

    MutexB.Release();
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Miguel
  • 143
  • 1
  • 7
  • As a side note, `async void` methods are intended mainly for event handlers, and generally [should be avoided](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void). – Theodor Zoulias Mar 31 '21 at 08:36
  • 1
    Yes, I didn't put it just to simplify, but they are event handlers. Thanks for the note though – Miguel Mar 31 '21 at 08:56

1 Answers1

1

My suggestion is to use two Channel<T> instances. The channel A should receive the arguments passed to the A event handler, and the channel B should receive the arguments passed to the B event handler. Finally perform an asynchronous loop that takes one item from the channel A and one from the channel B ad infinitum:

Channel<EventArgs> _channelA = Channel.CreateUnbounded<EventArgs>();
Channel<EventArgs> _channelB = Channel.CreateUnbounded<EventArgs>();

void EventA_Handler(EventArgs e)
{
    _channelA.Writer.TryWrite(e);
}

void EventB_Handler(EventArgs e)
{
    _channelB.Writer.TryWrite(e);
}

async void EventHandlerLoop()
{
    while (true)
    {
        EventArgs a = await _channelA.Reader.ReadAsync();
        // Do intended stuff...
        EventArgs b = await _channelB.Reader.ReadAsync();
        // Do intended stuff...
    }
}

The method EventHandlerLoop should be invoked during the initialization of the class, to start the wheel rolling.

If you are not familiar with the channels, you could take a look at this tutorial.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • I didn't know about the existence of `Channel`, thanks! Looks pretty much what I was looking for. Wouldn't be possible as well to include the content of the `while(true)` directly on `EventB_Handler`, below the writing to `_channelB`, in order to avoid the `EventHandlerLoop`? – Miguel Mar 31 '21 at 14:47
  • 1
    @Miguel maybe, but it would not be equally robust. The loop approach guarantees that the processing will happen sequentially in the intended order. Without a loop you have to make further assumptions about how the events are fired, and how the `Channel` behaves when consumed by multiple consumers. – Theodor Zoulias Mar 31 '21 at 21:51