I'm working on a game using the Godot game engine with Mono/C#. I'm trying to achieve the following:
Therefore I have a Say() method:
async Task Say(string msg)
{
  SetStatusText(msg);
  _tcs = new TaskCompletionSource<Vector2>();
  await _tcs.Task;
  SetStatusText(string.Empty);
}
What I'm expecting is this to work:
async Task Foo()
{
  // Displays "First".
  await Say("First");
  // "Second" should be shown after a click.
  await Say("Second");
  // "Third" should be shown after another click.
  await Say("Third");
}
What actually happens is:
I tracked it down to _tcs being null (or in an invalid state, if I don't set it to null) in my mouse button click code:
public void OnMouseButtonClicked(Vector2 mousePos)
{
  if(_tcs  != null)
  {
    _tcs.SetResult(mousePos);
    _tcs = null;
    return;
  }
  // Other code, executed if not using _tcs.
}
The mouse button click code sets the result of _tcs and this works fine for the first await, but then it fails, although I'm creating a new instance of TaskCompletionSource with every call of Say().
Godot problem or has my C# async knowledge become so rusty that I'm missing something here? It almost feels as if _tcs is being captured and reused.
has my C# async knowledge become so rusty that I'm missing something here?
It's a tricky corner of await: continuations are scheduled synchronously. I describe this more on my blog and in this single-threaded deadlock example.
The key takeaway is that TaskCompletionSource<T> will invoke continuations before returning, and this includes continuing methods that have awaited that task.
Walking through:
Foo invokes Say the first time.Say awaits _tcs.Task, which is not complete, so it returns an incomplete task.Foo awaits the task returned from Say, and returns an incomplete task.OnMouseButtonClicked is invoked.OnMouseButtonClicked calls _tcs.SetResult. This not only completes the task, it also runs the task's continuations.Say method is executed. If you place a breakpoint at SetStatusText(string.Empty), you'll see that the thread stack has OnMouseButtonClicked and SetResult in it!Say method, its task is completed, and that task's continuations are executed.Foo continues executing - from within OnMouseButtonClicked.Foo calls Say the second time, which sets _tcs and awaits the task. Since that task isn't complete, Say returns an incomplete task.Foo awaits that task, returning to OnMouseButtonClicked.OnMouseButtonClicked resumes executing after the SetResult line and sets _tcs to null.This kind of synchronous continuation doesn't always happen, but it's annoying when it does. One simple workaround is to pass TaskCreationOptions.RunContinuationsAsynchronously to the TaskCompletionSource<T> constructor.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With