【问题标题】:Async Await causing UI to be blocked [duplicate]异步等待导致 UI 被阻止 [重复]
【发布时间】:2019-10-29 21:40:12
【问题描述】:

我有一个问题,我有一个普通函数,它调用一个异步函数,它在循环中调用另一个异步函数。问题是,有对我的硬件接口的调用(例如执行电机运动的扫描),并且由于某种原因,这似乎会持续阻塞 UI 线程,直到我们真正完成整个“开始”函数的执行。

我不确定我做错了什么,但我觉得某些部分可能不应该在 UI 线程上完成,也许应该在另一个线程上完成......我不完全确定如何做到这一点。下面是代码示例。每次扫描时,UI 都会更新,每次扫描都会突出显示“按钮”。

    public void StartProcess(ObservableCollection<ObjModel> objs)
    {
        // Cancel
        if (cancellationTokenSource != null)
        {
            // Cancel tasks
            cancellationTokenSource.Cancel();
            cancellationTokenSource.Dispose();
            return;
        }

        cancellationTokenSource = new CancellationTokenSource();

        if (MachineController.Instance.InitHardware())
        {
            StartScan(objs);
        }
        else
        {
            Clean();
        }
    }

    private async void StartScan(ObservableCollection<ObjModel> objs)
    {
        // We are now running
        InfoModel.IsRunning = true;

        for (int index = 1; index < Size; index++)
        {
            MachineController.Instance.MoveToIndex(index - 1);
            if ((index - 1) % 2 == 0)
            {
                for (int iAIndex = 1; iAIndex < Size; iAIndex++)
                {
                    var obj = objs.Where(x => x.R == index && x.C == iAIndex).FirstOrDefault();

                    await DoScan(obj);
                }
            }
        }

        Clean();
    }

    private async Task DoScan(ObjModel obj)
    {
        MachineController.Instance.MoveToCIndex(obj.C - 1);

        // Set the task to only take a couple of seconds
        bool check = MachineController.Instance.Scan();

        if (check)
        {
            await Task.Delay(5, cancellationTokenSource.Token);

            // Plot  
            UpdateGraph(obj.R, obj.C);
        }
    }

【问题讨论】:

  • 不要使用async void 方法,事件处理程序除外。除此之外,MachineController.Instance.Scan() 显然在 UI 线程中被调用并阻塞了它。你也许可以把它包裹在await Task.Run(() =&gt; MachineController.Instance.Scan());
  • 嗨克莱门斯。谢谢。那我应该从 StartScan 函数中删除异步吗? DoScan 还能等待吗?...
  • 不,它应该声明为private async Task StartScan(...),并在调用时等待。就像DoScan。这同样适用于StartProcess
  • StartScan(objs); -> await StartScan(objs);
  • 唯一不会在调用线程上运行的代码是Task.Delay()(实际上我相信它会使用计时器)。事实上,您 awaitonly 为什么您的代码是异步的但没有达到您期望的水平的原因。其余的在调用线程上运行,这当然是 UI 线程,因此是阻塞的。你忘记使用异步 I/O 了吗? Task.Run()?

标签: c# wpf multithreading async-await


【解决方案1】:

MachineController.Instance 方法显然是在 UI 线程中调用的,可能会阻塞它。

为避免阻塞 UI 线程,请在 Task 中执行这些方法:

private async Task DoScan(ObjModel obj)
{
    bool check = await Task.Run(() =>
    {
        MachineController.Instance.MoveToCIndex(obj.C - 1);

        return MachineController.Instance.Scan();
    }

    ...
}

【讨论】:

  • 谢谢克莱门斯,我改变了它,以便我正在做你在上面做的事情。我无法更改原始的 async void 方法,因为当我在 StartScan 函数上调用 await 时,它需要原始调用函数是异步的才能使用 await ...我觉得我正在进入异步等待循环....
  • @uaswpff 你知道你可以在同步例程中使用Task someTask = Task.Run(() =&gt; { /* your code here */ }); someTask.Wait();,对吧?最好让你的整个调用链异步 - 甚至 UI 事件处理程序也可以是异步的 - 但在非常特殊的情况下,当你不能这样做时(比如在构造函数中),这就是你正在寻找的解决方案。
  • @mg30rg 这是一个糟糕的建议。永远不要在 UI 线程中调用 Task.Wait:stackoverflow.com/q/13411339/1136211。更好的是,永远不要使用它:stackoverflow.com/q/33351092/1136211
  • @Clemens 我强烈建议您在回答之前阅读整个评论。我写的是 最好让你的整个调用链异步 - 甚至 UI 事件处理程序也可以是异步的
  • @mg30rg 我当然这样做了,但是,这仍然是一个糟糕的建议,并且在这里无关紧要,因为根本没有任何特殊情况的迹象。问题中没有任何内容可以阻止 OP 使整个调用链异步。
【解决方案2】:

第一个选项是您实际上并没有在执行多任务处理。

但是还有另一种选择:您正在用更新淹没 GUI 线程,使其看起来像 GUI 线程被阻塞(当它真的很费力时)。

编写 GUI 是一项昂贵的操作。如果您只为每个用户触发的事件执行一次,您将永远不会注意到。但是从循环中执行它——尤其是在多任务中运行的循环——你会很快注意到它。在迈出多线程的第一步时,我实际上遇到了这个精确的问题。我更新 GUI 太频繁了,把它锁起来了。

我确实写了一些示例代码来展示它:

using System;
using System.Windows.Forms;

namespace UIWriteOverhead
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        int[] getNumbers(int upperLimit)
        {
            int[] ReturnValue = new int[upperLimit];

            for (int i = 0; i < ReturnValue.Length; i++)
                ReturnValue[i] = i;

            return ReturnValue;
        }

        void printWithBuffer(int[] Values)
        {
            textBox1.Text = "";
            string buffer = "";

            foreach (int Number in Values)
                buffer += Number.ToString() + Environment.NewLine;
            textBox1.Text = buffer;
        }

        void printDirectly(int[] Values){
            textBox1.Text = "";

            foreach (int Number in Values)
                textBox1.Text += Number.ToString() + Environment.NewLine;
        }

        private void btnPrintBuffer_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Generating Numbers");
            int[] temp = getNumbers(10000);
            MessageBox.Show("Printing with buffer");
            printWithBuffer(temp);
            MessageBox.Show("Printing done");
        }

        private void btnPrintDirect_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Generating Numbers");
            int[] temp = getNumbers(1000);
            MessageBox.Show("Printing directly");
            printDirectly(temp);
            MessageBox.Show("Printing done");
        }
    }
}

不对 Text 属性进行字符串连接可以将性能提高几个数量级。我不确定在这种情况下 Draw 行为有多智能:它会为每个 += 排队重绘,还是只为第一个重绘排队。但是对于多任务处理,您通常每次都会得到一个平局。

【讨论】:

  • 这个答案是各种无关紧要的。最大的问题:它根本不涉及异步/等待。下一个最大的问题:OP 的问题不涉及高频 UI 更新。第三大问题:在整个操作完成之前,上面的代码实际上并没有以任何一种方式更新 UI。第四大问题:示例在循环中使用字符串连接而不是StringBuilder
  • 我的意思是,如果一个人要回答一个根本不应该回答的问题(这个特定问题实际上已经在网站上重复了几十个,如果不是上百个),至少提供一个正确和有用的答案是有意义的。
  • @PeterDuniho HOW 你做多任务处理并不重要。没有什么比这个小细节相关了。
  • “你如何处理多任务并不重要”——确实如此。但是让我们假设你是对的。问题仍然是您的答案根本不提供 multitasking。您的示例代码中根本没有多任务处理。除了这个关键点之外,还有一个问题是您的答案解决了其他情况下可能存在的问题,但这与所提出的问题完全无关。 OP 的问题与 UI 更新的频率(您的说法)无关,而是他们实际上从未将长时间运行的操作放在后台线程中
  • @PeterDuniho “您的示例代码中根本没有多任务处理。”也许是因为它是为了证明编写 GUI 很昂贵?这就是为什么它被称为“UIWriteOverhead”。以及为什么将其写为我的第 3 段。在写这个例子时,我真的很惊讶它甚至不需要多任务处理。这使得代码更具可读性,同时仍然显示了重要的内容 - 在循环中写入 UI 类的令人难以置信的开销。
猜你喜欢
  • 1970-01-01
  • 2016-03-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-04
  • 2021-12-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多