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:
- Remove the
ConfigureAwait(false)
- 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.