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

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

我正在使用 Godot 游戏引擎和 Mono/C# 开发游戏。 我正在努力实现以下目标:

因此我有一个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");
}

实际发生的是:

我在鼠标按钮点击代码中追踪到 _tcsnull(或处于无效状态,如果我不将其设置为 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) 处放置一个断点,你会看到线程堆栈中有 OnMouseButtonClickedSetResult
  • Say 方法结束时,它的任务完成,并且 那个 任务的延续被执行。
  • 这意味着 Foo 继续执行 - 从 OnMouseButtonClicked.
  • Foo第二次调用Say,设置_tcs并等待任务。由于该任务未完成,Say returns 一项未完成的任务。
  • Foo 等待该任务,返回 OnMouseButtonClicked
  • OnMouseButtonClickedSetResult 行之后恢复执行并将 _tcs 设置为 null.

这种synchronous continuation doesn't always happen,但一发生就很烦人。一种简单的解决方法是将 TaskCreationOptions.RunContinuationsAsynchronously 传递给 TaskCompletionSource<T> 构造函数。