0

I'm using a third party lib which offer asynchronous methods. The lib can receive callbacks that will be executed during asynchronous operations.

I'm using this lib from a Windows Forms app and passing a callback to update UI state. However, it seems the callback is being called from a thread other then the UI's.

I've digged in the third party lib implementation, and they don't seem to be doing anything strange. Actually, they chain some async calls internally using ConfigureAwait(false), which seems to be the recommeded way of awaiting tasks inside libs.

I was able to reproduce the behavior with minimal code:

    public class ThirdPartyLibClass
    {
        public event EventHandler<EventArgs> StuffDone;
        public async Task DoStuffAsync()
        {
            await Task.Delay(100).ConfigureAwait(false);

            StuffDone?.Invoke(this, EventArgs.Empty);
        }
    }

    public partial class Form1 : Form
    {

        public Form1()
        {
            InitializeComponent();
        }

        private async void button2_Click(object sender, EventArgs e)
        {
            var thirdPartyLibClass = new ThirdPartyLibClass();
            thirdPartyLibClass.StuffDone += (s,e2) => this.textBox1.Text = "This will fail because it will be executed in a thread other than the UI's."; 
            
            await thirdPartyLibClass.DoStuffAsync();
        }
    }

I know how to get around the issue.

The point is: it seems counterintuitive to force a lib consumer to assume that callbacks in async libs might not be called in the original synchronization context.

I've considered the following ways to better deal with this, from the lib maintainer's perspective:

  1. Remove the ConfigureAwait(false)
  2. Capture the synchronization context and raise any events / call any callbacks on it

My questions is: should async libs make sure that callbacks are executed in their original synchronization context, or should consumers of async libs always consider that events/callbacks might be called from unknown threads?

Update

Based on Stephen's answer, I realized my example was a bit biased towards watch completion, which is not the case I'd like to bring to light.

I've provided an updated example below:

    public class ThirdPartyLibClass
    {
        private Action doSomeMoreStuffForMeAsync;

        public ThirdPartyLibClass(Action doSomeMoreStuffForMeAsync)
        {
            this.doSomeMoreStuffForMeAsync = doSomeMoreStuffForMeAsync;
        }

        public async Task DoStuffAsync()
        {
            await Task.Delay(100).ConfigureAwait(false);

            doSomeMoreStuffForMeAsync();

            await Task.Delay(100).ConfigureAwait(false);
        }
    }

    public partial class Form1 : Form
    {

        public Form1()
        {
            InitializeComponent();
        }

        private async void button2_Click(object sender, EventArgs e)
        {
            var thirdPartyLibClass = new ThirdPartyLibClass(
                () => this.textBox1.Text = "This will fail because it will be executed in a thread other than the UI's."
            );
            
            await thirdPartyLibClass.DoStuffAsync();
        }
    }

To be more concrete, the third party library I'm using is Stateless. Upon the state machine construction, it receives delegates to be called on state transitions. However, I'm finding out that I cannot rely on those delegates being called in the original synchronization context, which is a real bummer, as I essentially wanted to use a state machine to organize better my GUI applications.

Arthur Nunes
  • 6,718
  • 7
  • 33
  • 46
  • Pretty common scenario. Add `IProgress textBoxUpdater = null;`, in, e.g., the Click handler: `textBoxUpdater = new Progress((s) => txtSysEvtLogger.Text = s); thirdPartyLibClass.StuffDone += (s, e2) => textBoxUpdater.Report("text"); await thirdPartyLibClass.DoStuffAsync();` -- You probably don't want that code to post to the Synch Context, you probably prefer to elaborate the results and, eventually, update the UI, if you need to. Maybe you don't, so why bother your UI Thread. Not everyone needs to update an UI – Jimi Nov 03 '22 at 03:30
  • @jimi Microsoft's Progress class was exactly one of the examples I had in mind when thinking on the approach (2). I've used it in similar scenarios and stumbled upon the built in synchronization behavior: https://learn.microsoft.com/en-us/dotnet/api/system.progress-1?view=net-6.0#remarks – Arthur Nunes Nov 03 '22 at 12:11
  • If you're referring to a library, you don't have to do anything on that side. Most of the libraries that signals *progress* through events (e.g., all .Net libraries I know of that capture camera feeds) raise the event in a ThreadPool Thread. This is what you want to happen, since you may need to treat the frame before you render it in a UI, so you marshal the result when you're done; or you may discard it. The application that uses the library may not have a synchronization context at all, you have no reason to try and capture it yourself, that's where you introduce *mutant* behavior – Jimi Nov 03 '22 at 12:34

1 Answers1

1

Asynchronous libraries using async/await usually don't use events to signal completion at all. Using events to signal completion is likely a holdover from an old version of the API, which was probably using the Event-Based Asynchronous Pattern.

If that's correct, and if the APIs return tasks, then it seems like your library is exposing some kind of mixed API. In that case, you should just ignore any completion events and use await to handle completion instead:

private async void button2_Click(object sender, EventArgs e)
{
  var thirdPartyLibClass = new ThirdPartyLibClass();
  await thirdPartyLibClass.DoStuffAsync();
  this.textBox1.Text = "This works fine.";
}

should async libs make sure that callbacks are executed in their original synchronization context, or should consumers of async libs always consider that events/callbacks might be called from unknown threads?

Most events are just raised on a thread pool context, and it is up to the handler to do any thread marshalling necessary.

There are a few exceptions; some objects capture SynchronizationContext.Current at some point and use that to raise events - but in that case you need to be sure to document when it is captured (e.g., the constructor vs DoStuffAsync). There was also an old pattern based on ISynchronizeInvoke with a property commonly called SynchronizingObject. But in modern code, events are generally expected to be raised on a thread pool thread.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • I've updated my question, I've realized I had not chosen a good example. The focus of my question is more on libs that receive delegates that will be called during the execution of their async methods, as part of the whole async operation. I'm already making sure that, from my code, all lib's async APIs are called using async/await. – Arthur Nunes Nov 03 '22 at 12:22