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 await
ed 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