C#等待任务+无限循环仍然冻结用户界面

时间:2016-04-24 01:34:57

标签: c# .net async-await task-parallel-library

我正在努力寻找合适的结构'用于监视来自外部源的游戏状态使用(任务)async / await以便在无限循环中运行任务,但是其编写的当前方式似乎只是冻结了我的UI。

到目前为止我所拥有的:

(在"状态机"类中)

// 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"它没有冻结,但如果我试图拖动窗口,它会跳回到我开始拖动它的位置。

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

CheckForStateChange()签名如下:

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

没有什么特别的,简单的非异步方法。我已经在StackOverflow上阅读了很多示例/问题,我曾经将CheckForStateChange作为返回一个Task(在方法中有等待的动作)和许多其他代码迭代(具有相同的结果)。

最后,我从主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();
    }
}

我认为上面的代码是迄今为止我编写代码结构的最简单的代码,但显然它仍然没有写好'正确'。任何帮助将不胜感激。

更新: 发布者方(状态机类):

    // ------ 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()方法只是一种检索“文本”的临时方法。当前状态的表示(此时它实际上是一个占位符,直到我将它组织成一个更整洁的结构)

IsRunning纯粹是一个公共布尔。

UI线程中的处理程序:

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

1 个答案:

答案 0 :(得分:2)

为什么用户界面冻结

就主要问题而言:您已经通过CheckForStateChange致电Task.Run,因此无法您的CheckForStateChange会冻结UI,除非它包含编组回UI线程的调用(即显式使用Control.InvokeSynchronizationContext.Post/Send,或通过UI Task上启动的TaskScheduler隐式调用。 / p>

开始寻找的最佳位置是StateChange处理程序(即StateChangeHandler)。另请查看StateChange事件的引发位置。您可以在其中一个站点找到线程编组代码。

其他问题

您将指向UI TaskScheduler的{​​{1}}传递给外部任务。你也传递了SynchronizationContext。简单来说,您告诉任务工厂“在专用线程和当前线程上启动任务”。这两个是相互排斥的要求,你可以非常安全地放弃它们。

如果由于上述原因,您的外部任务恰好在UI线程上执行,那么内部调用将包含在TaskCreationOptions.LongRunning中,它不会真正使您失望,但这可能不是你期望的行为。

您将Task.Run的结果存储在Task.Factory.StartNew字段或属性中。不过请注意,您的task调用会返回Task.Factory.StartNew,因此保存的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实现。挥发性状态很难做到。如果框架是免费提供给您的,您应该使用它。

最后一句话

总的来说,整个解决方案对于应该更加反应性的事情感觉有点过分 - 但我可以想到这种设计有效的场景。我只是不相信你的确是其中之一。

编辑:如何找到阻止用户界面的内容

我会为此遗忘,但这里有:

找到导致UI线程回发的原因的确定方法是使用它进行死锁。这里有很多线索告诉你如何避免这种情况,但在你的情况下 - 我们会故意这样做,你会确切地知道你在轮询变化时需要避免哪些呼叫 - 尽管是否是可以避免这些电话,还有待观察。

我在代码片段末尾添加了IsRunning条指令。如果您在UI线程上调用task.Wait,这会导致Start内的某事出现死锁,并且您将知道需要解决的问题。