TaskCompletionSource 无法处理多个等待的任务 - 我做错了什么?



我正在开发一款使用带有Mono/C#的Godot游戏引擎的游戏。 我正在尝试实现以下目标:

  • 在屏幕上显示消息
  • 等待鼠标按钮点击/屏幕点击
  • 显示另一条消息
  • 等待点击

因此,我有一个Say()方法:

async Task Say(string msg)
{
SetStatusText(msg);
_tcs = new TaskCompletionSource<Vector2>();
await _tcs.Task;
SetStatusText(string.Empty);
}

我期望的是这个工作:

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");
}

实际发生的是:

  • 显示"第一个">
  • 单击后显示"秒"。
  • "第三"永远不会显示,即使在点击后也是如此。

我在鼠标按钮单击代码中将其追踪到null_tcs(或处于无效状态,如果我不将其设置为null):

public void OnMouseButtonClicked(Vector2 mousePos)
{
if(_tcs  != null)
{
_tcs.SetResult(mousePos);
_tcs = null;
return;
}
// Other code, executed if not using _tcs.
}

鼠标按钮单击代码设置_tcs的结果,这对于第一个await工作正常,但随后失败,尽管我每次调用Say()都会创建一个新的TaskCompletionSource实例。

戈多问题还是我的 C# 异步知识变得如此生疏以至于我在这里错过了一些东西?几乎感觉_tcs被捕获并重复使用。

我的 C# 异步知识是否变得如此生疏,以至于我在这里错过了一些东西?

这是一个棘手的await:延续是同步安排的。我在我的博客和这个单线程死锁示例中对此进行了更多描述。

关键要点是,TaskCompletionSource<T>将在返回之前调用延续,这包括await该任务的继续方法。

演练:

  • Foo第一次调用Say
  • Say等待未完成的_tcs.Task,因此返回未完成的任务。
  • Foo等待从Say返回的任务,并返回一个未完成的任务。
  • 用户单击并调用OnMouseButtonClicked
  • OnMouseButtonClicked打电话给_tcs.SetResult.这不仅可以完成任务,还可以运行任务的延续。
  • 这意味着将执行Say方法的其余部分。如果在SetStatusText(string.Empty)放置断点,您将看到线程堆栈中包含OnMouseButtonClickedSetResult
  • Say方法结束时,其任务完成,并执行任务的延续。
  • 这意味着Foo继续执行 - 从OnMouseButtonClicked
  • Foo第二次调用Say,这会设置_tcs并等待任务。由于该任务未完成,因此Say返回未完成的任务。
  • Foo等待着这项任务,回到OnMouseButtonClicked.
  • OnMouseButtonClickedSetResult行之后恢复执行,并将_tcs设置为null

这种同步延续并不总是发生,但当它发生时很烦人。一个简单的解决方法是将TaskCreationOptions.RunContinuationsAsynchronously传递给TaskCompletionSource<T>构造函数。

Jon 的评论让我重新考虑代码,最终我改变了它。 我现在有一个队列,而不是一个TaskCompletionSource

Queue<TaskCompletionSource<Vector2>> _tcsQueue = new Queue<TaskCompletionSource<Vector2>>();

对于每个对Say()的调用,都会添加一个新的新TaskComletionSource

async Task Say(string msg)
{
SetStatusText(msg);
var tcs = new TaskCompletionSource<Vector2>();
_tcsQueue.Enqueue(tcs);
await tcs.Task;
SetStatusText(string.Empty);
}

在单击事件处理程序中,我将最旧的 TCS 取消排队并设置其结果:

public void OnMouseButtonClicked(Vector2 mousePos)
{
if(_tcsQueue.Count > 0)
{
var tcs = _tcsQueue.Dequeue();
tcs.SetResult(mousePos);
return;
}
// Other code.
}

这确保了对于多次调用Say(),每次点击都会继续到下一次。

相关内容

最新更新