简体   繁体   English

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

[英]C# await tasks + infinite loop still freezing the UI

I am trying to get the proper 'structure' for monitoring the state of a game from external source(s) using (Tasks) async/await in order to run the tasks in an infinite loop, however the current way its written seems to just freeze up my UI. 我正在尝试使用(Tasks)async / await从外部源获取适当的'结构'来监视游戏的状态,以便无限循环地运行任务,但是其当前编写方式似乎只是冻结我的用户界面。

What I have so far: 到目前为止,我有:

(in the "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());
}

Without the above "Task.Delay" line the UI completely freezes up. 没有上面的“ Task.Delay”行,UI将完全冻结。 With the "Task.Delay" line it doesn't freeze up, but if I try to drag the window it skips back to where I began dragging it. 使用“ Task.Delay”行不会冻结,但是如果我尝试拖动窗口,它将跳回到我开始拖动它的位置。

My assumption with the current code is that the 'await Task.Run()' executes and upon completion the 'await Task.Delay()' executes and then on completion returns to the beginning of the while(true) infinite loop. 我对当前代码的假设是执行“ await Task.Run()”,并在完成时执行“ await Task.Delay()”,然后在完成时返回while(true)无限循环的开始。 (ie. not running in parallel). (即不并行运行)。

The CheckForStateChange() signature is as follows: CheckForStateChange()签名如下:

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

Nothing special there, simple non-async method. 没什么特别的,简单的非异步方法。 I have read through lots of examples / questions here on StackOverflow and I used to have CheckForStateChange as returning a Task (with awaitable actions inside the method) and many other iterations of code (with the same results). 我在此处阅读了有关StackOverflow的大量示例/问题,并且我曾经让CheckForStateChange返回一个Task(方法中包含等待的动作)和许多其他代码迭代(具有相同的结果)。

Finally I call the Start() method from the main win32 form (button) as follows: 最后,我从主要的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();
    }
}

I think the above code is the simplest form I have written the code structure in so far, but apparently its still not written 'properly'. 我认为以上代码是到目前为止我编写代码结构的最简单形式,但是显然它仍然不是“正确”编写的。 Any help would be appreciated. 任何帮助,将不胜感激。

UPDATE: The publisher side (state machine 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());
    }

Where the StateText() method is just a temporary way of retrieving a 'text' representation of the current state (and is really a placeholder at this point until I organize it into a tidier struct) 其中StateText()方法只是检索当前状态的“文本”表示形式的一种临时方法(在此刻它实际上是一个占位符,直到我将其组织为整齐的结构)

IsRunning is purely a public bool. IsRunning纯粹是一个公共布尔。

And the handler in the UI thread: UI线程中的处理程序:

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

Why the UI freezes 用户界面为何冻结

In terms of the main question: you're already calling your CheckForStateChange via Task.Run , so there is no way that your CheckForStateChange will freeze the UI unless it includes calls which are marshalled back to the UI thread (ie Control.Invoke or SynchronizationContext.Post/Send used explicitly, or implicitly via a Task started on the UI TaskScheduler ). 在主要问题方面:您已经通过Task.Run调用了CheckForStateChange ,因此除非您的CheckForStateChange包含被编组回UI线程的调用(即Control.InvokeSynchronizationContext.Post/Send否则您将无法冻结UI。 SynchronizationContext.Post/Send可以显式使用,也可以通过在UI TaskScheduler上启动的Task隐式使用。

The best place to start looking is your StateChange handlers (ie StateChangeHandler ). 开始寻找的最佳位置是StateChange处理程序(即StateChangeHandler )。 Also have a look at where the StateChange event is raised. 还可以看看StateChange事件在何处引发。 You'll find thread marshalling code at one of these sites. 您可以在这些站点之一中找到线程编组代码。

Other issues 其他事宜

You're passing the TaskScheduler pointing to the UI SynchronizationContext to the outer task. 您正在将指向UI SynchronizationContextTaskScheduler传递给外部任务。 You're also passing in TaskCreationOptions.LongRunning . 您还传递了TaskCreationOptions.LongRunning In simple terms you're telling the task factory to "start a task on a dedicated thread, and on the current thread". 简单来说,您是在告诉任务工厂“在专用线程和当前线程上启动任务”。 These two are mutually exclusive requirements and you can pretty safely drop them both. 这两个是互斥的要求,您可以放心地将它们都删除。

If, as a result of the above, your outer task happens to execute on the UI thread, it won't really trip you up as the inner call is wrapped in Task.Run , but this probably isn't the behaviour you expect. 由于上述原因,如果您的外部任务恰好在UI线程上执行,则不会真正使您绊倒,因为内部调用包含在Task.Run ,但这可能不是您期望的行为。

You are storing the result of Task.Factory.StartNew inside a task field or property. 您将Task.Factory.StartNew的结果存储在task字段或属性中。 Note, however, that your Task.Factory.StartNew call returns a Task<Task> , so the saved Task instance will transition to completed state almost immediately unless you call Unwrap on it and get to the inner task. 但是请注意,您的Task.Factory.StartNew调用返回Task<Task> ,因此,除非您对其调用Unwrap并进入内部任务,否则保存的Task实例将几乎立即转换为完成状态。 To avoid this entire mess, just use Task.Run to create the outer task (as it has Unwrap semantics built in). 为了避免整个混乱,只需使用Task.Run创建外部任务(因为它内置了Unwrap语义)。 If you do that, you can ditch the inner Task.Run completely, like so: 如果这样做,您可以Task.Run内部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();
}

Since you have a CancellationToken you need to be passing it to CheckForStateChange , and checking it periodically - otherwise it only gets checked once, when the Task is started, and then never again. 由于您具有CancellationToken ,因此需要将其传递给CheckForStateChange ,并定期对其进行检查-否则,仅在启动Task时对其进行一次检查,然后再也不会进行检查。

Note that I have also provided a different IsRunning implementation. 请注意,我还提供了一个不同的IsRunning实现。 Volatile state is hard to get right. 不稳定的状态很难纠正。 If the framework is giving it to you for free, you should use it. 如果该框架免费提供给您,则应使用它。

Final word 最后的话

Overall this entire solution feels like a bit of a crutch for something that should be done more reactively - but I can think of scenarios where this sort of design is valid. 总体而言,整个解决方案似乎有些a脚,应该以更被动的方式完成某些工作-但我可以想到这种设计有效的方案。 I'm just not convinced that yours is really one of them. 我只是不相信您的人确实是其中之一。

EDIT: how to find what's blocking the UI 编辑:如何查找阻止用户界面的内容

I'll get downvoted to oblivion for this, but here goes: 我将对此表示down惜,但这里有:

The sure way to find what's causing postbacks to the UI thread is to deadlock with it. 确定导致UI线程回发的原因的肯定方法是对其进行死锁。 There's plenty of threads here on SO telling you how to avoid that, but in your case - we'll cause it on purpose and you'll know exactly what calls you need to avoid when you're polling for changes - although whether or not it will be possible to avoid these calls, remains to be seen. SO上有很多线程告诉您如何避免这种情况,但是就您而言-我们会故意使它发生,并且您将确切地知道在轮询更改时需要避免的调用-尽管是否有可能避免这些呼吁,还有待观察。

I've put a task.Wait instruction at the end of my code snippet. 我已经放置了一个task.Wait指令,在我的代码片段的末尾。 Provided that you call Start on the UI thread, that should cause a deadlock with something inside your CheckForStateChange , and you will know what it is that you need to work around. 前提是您在UI线程上调用Start ,这将导致CheckForStateChange 某些内容导致死锁,并且您将知道需要解决的问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM