我正在开发一款使用带有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)
放置断点,您将看到线程堆栈中包含OnMouseButtonClicked
和SetResult
! - 在
Say
方法结束时,其任务完成,并执行该任务的延续。 - 这意味着
Foo
继续执行 - 从OnMouseButtonClicked
内。 Foo
第二次调用Say
,这会设置_tcs
并等待任务。由于该任务未完成,因此Say
返回未完成的任务。Foo
等待着这项任务,回到OnMouseButtonClicked
.OnMouseButtonClicked
在SetResult
行之后恢复执行,并将_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()
,每次点击都会继续到下一次。