C# 等待任务 + 无限循环仍然冻结 UI

C# await tasks + infinite loop still freezing the UI

我正在尝试使用(任务)async/await 从外部来源获得适当的 'structure' 来监控游戏状态,以便 运行 中的任务一个无限循环,但是它的当前编写方式似乎只是冻结了我的 UI.

我目前拥有的:

(在"state machine" class)

// Start monitoring the game state for changes
public void Start()
{
    tokenSource = new CancellationTokenSource();
    CancellationToken token = tokenSource.Token;
    IsRunning = true;
    task = Task.Factory.StartNew(async () =>
    {
        while (true)
        {
            await Task.Run(()=>CheckForStateChange());
            await Task.Delay(1000); // Pause 1 second before checking state again
        }
    }, token, TaskCreationOptions.LongRunning, TaskScheduler.FromCurrentSynchronizationContext());
}

如果没有上面的 "Task.Delay" 行,UI 就会完全冻结。使用 "Task.Delay" 行它不会冻结,但如果我尝试拖动 window 它会跳回到我开始拖动它的位置。

我对当前代码的假设是 'await Task.Run()' 执行并在完成时执行 'await Task.Delay()' 然后在完成时 returns 到 while(true) 无限循环的开始. (即不是 运行 并行)。

CheckForStateChange() 签名如下:

private void CheckForStateChange()
{
    // ... A bunch of code to determine and update the current state value of the object
}

没什么特别的,简单的非异步方法。我已经在 Whosebug 上阅读了很多示例/问题,并且我曾经将 CheckForStateChange 作为返回任务(在方法内具有可等待的操作)和许多其他代码迭代(具有相同的结果)。

最后,我从主 win32 窗体(按钮)调用 Start() 方法,如下所示:

private void btnStartSW_Click(object sender, EventArgs e)
{
    // Start the subscription of the event handler
    if(!state.IsRunning)
    {
        state.StateChange += new SummonersWar.StateChangeHandler(OnGameStateChange);
        state.Start();
    }
}

我认为上面的代码是我目前写的代码结构中最简单的形式,但显然还没有写好'properly'。任何帮助将不胜感激。

更新: 发布方(状态机class):

    // ------ Publisher of the event ---
    public delegate void StateChangeHandler(string stateText);
    public event StateChangeHandler StateChange;
    protected void OnStateChange() // TODO pass text?
    {
        if (StateChange != null)
            StateChange(StateText());
    }

其中 StateText() 方法只是检索 'text' 表示当前状态的一种临时方式(在我将其组织成更整洁的结构之前,它实际上是一个占位符)

IsRunning 纯粹是一个 public 布尔值。

并且 UI 线程中的处理程序:

private void OnGameStateChange(string stateText)
{
    // Game State Changed (update the status bar)
    labelGameState.Text = "State: " + stateText;
}

为什么 UI 冻结

就主要问题而言:您已经通过 Task.Run 呼叫您的 CheckForStateChange,因此 不可能 您的 [=11] =] 将冻结 UI 除非它包括编组回 UI 线程的调用(即 Control.InvokeSynchronizationContext.Post/Send 显式使用,或通过 [=16= 隐式使用] 开始于 UI TaskScheduler).

开始查找的最佳位置是您的 StateChange 处理程序(即 StateChangeHandler)。还可以查看引发 StateChange 事件的位置。您将在这些站点之一找到线程编组代码。

其他问题

您正在将指向 UI SynchronizationContextTaskScheduler 传递给外部任务。您还传入 TaskCreationOptions.LongRunning。简单来说,您是在告诉任务工厂 "start a task on a dedicated thread, and on the current thread"。这两个是互斥的要求,您可以非常安全地放弃它们。

如果,作为上述结果,你的外部任务恰好在 UI 线程上执行,它不会真正绊倒你,因为内部调用包含在 Task.Run 中,但这可能不是您期望的行为。

您正在将 Task.Factory.StartNew 的结果存储在 task 字段或 属性 中。但是请注意,您的 Task.Factory.StartNew 调用 returns 一个 Task<Task>,因此保存的 Task 实例将几乎立即转换为完成状态,除非您对其调用 Unwrap并开始内部任务。为了避免这整个混乱,只需使用 Task.Run 创建外部任务(因为它内置了 Unwrap 语义)。如果你这样做,你可以完全放弃内部 Task.Run,像这样:

public bool IsRunning
{
    get
    {
        return task.Status == TaskStatus.Running;
    }
}

public void Start()
{
    tokenSource = new CancellationTokenSource();
    CancellationToken token = tokenSource.Token;

    task = Task.Run(async () =>
    {
        while (true)
        {
            CheckForStateChange(token);

            token.ThrowIfCancellationRequested();

            await Task.Delay(1000); // Pause 1 second before checking state again
        }
    }, token);

    // Uncomment this and step through `CheckForStateChange`.
    // When the execution hangs, you'll know what's causing the
    // postbacks to the UI thread and *may* be able to take it out.
    // task.Wait();
}

因为你有一个 CancellationToken 你需要将它传递给 CheckForStateChange,并定期检查它 - 否则它只会在 Task 启动时检查一次,并且然后再也不会了。

请注意,我还提供了不同的 IsRunning 实现。不稳定的状态很难正确。如果框架免费提供给你,你应该使用它。

结语

总的来说,整个解决方案对于一些应该更被动地完成的事情来说感觉有点像拐杖 - 但我可以想到这种设计有效的场景。我只是不相信你真的是其中之一。

编辑:如何找到阻塞 UI

的内容

我会因为这个而被遗忘,但这里是:

找出导致回发到 UI 线程的原因的可靠方法是与其死锁。 SO 上有很多线程告诉你如何避免这种情况,但在你的情况下 - 我们会故意引起它并且你会确切地知道当你轮询更改时你需要避免什么调用 - 尽管是否是否有可能避免这些调用,还有待观察。

我在我的代码片段末尾放了一条 task.Wait 指令。如果您在 UI 线程上调用 Start,那应该会导致 something 在您的 CheckForStateChange 中出现死锁,您将知道它是什么你需要变通。