Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TaskCompletionSource not working with more than one awaited Task - what am I doing wrong?

I'm working on a game using the Godot game engine with Mono/C#. I'm trying to achieve the following:

  • Display a message on screen
  • Wait for a mouse button click/screen tap
  • Display another message
  • Wait for click
  • ...

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:

  • "First" is shown
  • "Second" is shown after a click.
  • "Third" never shows up, even after a click.

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.

like image 697
Krumelur Avatar asked Oct 15 '25 07:10

Krumelur


1 Answers

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.
  • The user clicks and OnMouseButtonClicked is invoked.
  • OnMouseButtonClicked calls _tcs.SetResult. This not only completes the task, it also runs the task's continuations.
  • This means that the remainder of the 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!
  • At the end of the Say method, its task is completed, and that task's continuations are executed.
  • This means 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.

like image 106
Stephen Cleary Avatar answered Oct 20 '25 00:10

Stephen Cleary



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!