TaskCompletionSource 不处理多个等待的任务 - 我做错了什么?
TaskCompletionSource not working with more than one awaited Task - what am I doing wrong?
我正在使用 Godot 游戏引擎和 Mono/C# 开发游戏。
我正在努力实现以下目标:
- 在屏幕上显示消息
- 等待鼠标按钮click/screen点击
- 显示另一条消息
- 等待点击
- ...
因此我有一个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");
}
实际发生的是:
- 显示“第一个”
- 点击后显示“第二个”。
- “第三个”永远不会出现,即使在点击后也是如此。
我在鼠标按钮点击代码中追踪到 _tcs
为 null
(或处于无效状态,如果我不将其设置为 null
):
public void OnMouseButtonClicked(Vector2 mousePos)
{
if(_tcs != null)
{
_tcs.SetResult(mousePos);
_tcs = null;
return;
}
// Other code, executed if not using _tcs.
}
鼠标按钮单击代码设置了 _tcs
的结果,这对第一个 await
工作正常,但后来失败了,尽管我正在创建 [=19= 的新实例] 每次调用 Say()
.
Godot 问题或我的 C# 异步知识变得如此生疏以至于我在这里遗漏了什么?几乎感觉好像 _tcs
被捕获并重复使用。
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()
,每次点击都会进行到下一次。
has my C# async knowledge become so rusty that I'm missing something here?
这是 await
的一个棘手的角落:延续是同步安排的。我再描述一下on my blog and in this single-threaded deadlock example。
关键要点是 TaskCompletionSource<T>
将在返回 之前调用延续 ,这包括 await
完成该任务的延续方法。
走过:
Foo
第一次调用 Say
。
Say
等待 _tcs.Task
,未完成,因此 returns 任务未完成。
Foo
等待从 Say
返回的任务,returns 一个未完成的任务。
- 用户点击并调用
OnMouseButtonClicked
。
OnMouseButtonClicked
呼叫 _tcs.SetResult
。这不仅完成了任务,还运行了任务的延续。
- 这意味着
Say
方法的剩余部分被执行。如果你在 SetStatusText(string.Empty)
处放置一个断点,你会看到线程堆栈中有 OnMouseButtonClicked
和 SetResult
!
- 在
Say
方法结束时,它的任务完成,并且 那个 任务的延续被执行。
- 这意味着
Foo
继续执行 - 从 OnMouseButtonClicked
.
Foo
第二次调用Say
,设置_tcs
并等待任务。由于该任务未完成,Say
returns 一项未完成的任务。
Foo
等待该任务,返回 OnMouseButtonClicked
。
OnMouseButtonClicked
在 SetResult
行之后恢复执行并将 _tcs
设置为 null
.
这种synchronous continuation doesn't always happen,但一发生就很烦人。一种简单的解决方法是将 TaskCreationOptions.RunContinuationsAsynchronously
传递给 TaskCompletionSource<T>
构造函数。
我正在使用 Godot 游戏引擎和 Mono/C# 开发游戏。 我正在努力实现以下目标:
- 在屏幕上显示消息
- 等待鼠标按钮click/screen点击
- 显示另一条消息
- 等待点击
- ...
因此我有一个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");
}
实际发生的是:
- 显示“第一个”
- 点击后显示“第二个”。
- “第三个”永远不会出现,即使在点击后也是如此。
我在鼠标按钮点击代码中追踪到 _tcs
为 null
(或处于无效状态,如果我不将其设置为 null
):
public void OnMouseButtonClicked(Vector2 mousePos)
{
if(_tcs != null)
{
_tcs.SetResult(mousePos);
_tcs = null;
return;
}
// Other code, executed if not using _tcs.
}
鼠标按钮单击代码设置了 _tcs
的结果,这对第一个 await
工作正常,但后来失败了,尽管我正在创建 [=19= 的新实例] 每次调用 Say()
.
Godot 问题或我的 C# 异步知识变得如此生疏以至于我在这里遗漏了什么?几乎感觉好像 _tcs
被捕获并重复使用。
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()
,每次点击都会进行到下一次。
has my C# async knowledge become so rusty that I'm missing something here?
这是 await
的一个棘手的角落:延续是同步安排的。我再描述一下on my blog and in this single-threaded deadlock example。
关键要点是 TaskCompletionSource<T>
将在返回 之前调用延续 ,这包括 await
完成该任务的延续方法。
走过:
Foo
第一次调用Say
。Say
等待_tcs.Task
,未完成,因此 returns 任务未完成。Foo
等待从Say
返回的任务,returns 一个未完成的任务。- 用户点击并调用
OnMouseButtonClicked
。 OnMouseButtonClicked
呼叫_tcs.SetResult
。这不仅完成了任务,还运行了任务的延续。- 这意味着
Say
方法的剩余部分被执行。如果你在SetStatusText(string.Empty)
处放置一个断点,你会看到线程堆栈中有OnMouseButtonClicked
和SetResult
! - 在
Say
方法结束时,它的任务完成,并且 那个 任务的延续被执行。 - 这意味着
Foo
继续执行 - 从OnMouseButtonClicked
. Foo
第二次调用Say
,设置_tcs
并等待任务。由于该任务未完成,Say
returns 一项未完成的任务。Foo
等待该任务,返回OnMouseButtonClicked
。OnMouseButtonClicked
在SetResult
行之后恢复执行并将_tcs
设置为null
.
这种synchronous continuation doesn't always happen,但一发生就很烦人。一种简单的解决方法是将 TaskCreationOptions.RunContinuationsAsynchronously
传递给 TaskCompletionSource<T>
构造函数。