如何阻止我的 UI 冻结?

Posted

技术标签:

【中文标题】如何阻止我的 UI 冻结?【英文标题】:How do I Stop my UI from freezing? 【发布时间】:2018-09-26 07:21:57 【问题描述】:

我正在为我的大学学位创建一个远程管理工具。我目前确实遇到了代码中的一个错误,想知道是否有人可以对我的问题有所了解。

我有应用程序、服务器和客户端。服务器运行良好。但是客户端是冻结的应用程序。

在连接到服务器之前,客户端可以正常工作。当连接到服务器时,客户端一直在屏幕上冻结。

我已将错误范围缩小到特定代码段,如果没有运行此代码,应用程序不会冻结。但是,该应用程序也不起作用。

这是该代码的示例:

private static void ReceiveResponse()
    

        var buffer = new byte[2048]; //The receive buffer

        try
        
            int received = 0;
            if (!IsLinuxServer) received = _clientSocket.Receive(buffer, SocketFlags.None); //Receive data from the server
            else received = _sslClient.Read(buffer, 0, 2048);
            if (received == 0) return; //If failed to received data return
            var data = new byte[received]; //Create a new buffer with the exact data size
            Array.Copy(buffer, data, received); //Copy from the receive to the exact size buffer

            if (isFileDownload) //File download is in progress
            
                Buffer.BlockCopy(data, 0, recvFile, writeSize, data.Length); //Copy the file data to memory

                writeSize += data.Length; //Increment the received file size

                if (writeSize == fup_size) //prev. recvFile.Length == fup_size
                


                    using (FileStream fs = File.Create(fup_location))
                    
                        Byte[] info = recvFile;
                        // Add some information to the file.
                        fs.Write(info, 0, info.Length);
                    

                    Array.Clear(recvFile, 0, recvFile.Length);
                    SendCommand("frecv");
                    writeSize = 0;
                    isFileDownload = false;
                    return;
                
            

            if (!isFileDownload) //Not downloading files
            
                string text = (!IsLinuxServer) ? Encoding.Unicode.GetString(data) : Encoding.UTF8.GetString(data); //Convert the data to unicode string
                string[] commands = GetCommands(text); //Get command of the message



                foreach (string cmd in commands) //Loop through the commands
                
                    HandleCommand(Decrypt(cmd)); //Decrypt and execute the command
                
            
        
        catch (Exception ex) //Somethind went wrong
        
            MessageBox.Show(ex.Message);
            RDesktop.isShutdown = true; //Stop streaming remote desktop
           // MessageBox.Show("Connection ended");
        
    

此代码是用户如何接收来自服务器的请求。所以它在一个定时器中每 100 毫秒运行一次。

我想知道计时器是否与它有关。在它处于 While(true) 循环之前,我遇到了同样的问题,这让我认为这是导致 UI 冻结的实际代码。

即使应用程序被冻结,代码仍然可以工作,应用程序上的所有内容都可以正常工作,除了 UI。

这真的令人沮丧,我真的看不出代码有什么问题会导致应用程序冻结。

任何帮助将不胜感激。

提前谢谢你。

【问题讨论】:

你需要以asynchronous的方式运行这段代码。 您在 UI 线程上执行的任何操作都会阻止 UI 线程执行其他任何操作。这就是为什么你不应该做任何在 UI 线程上运行很长时间的事情。如果您需要收集数据,请在辅助线程上执行此操作,然后仅编组回 UI 线程以实际更新 UI。阅读有关并行、多线程和异步编程的知识。 你写if (x) ... if (!x) ... 而不是if (x) ... else ... ,你觉得奇怪吗?我觉得很奇怪。为什么选择这种方式来编写 if-else? 更一般地说,在我看来,这段代码正在做四件事:(1) 通过普通套接字下载文件,(2) 通过 ssl 套接字下载文件,(3) 通过普通套接字,(4) 通过 ssl 套接字下载文本。这里似乎缺少一些可以从现有部分轻松构建的抽象,这将使这段代码更简单、更容易理解。 计算机编程是制作有用抽象的艺术。您的代码在这里缺乏抽象。 考虑会发生什么是请求需要 > 100 毫秒。 【参考方案1】:

到目前为止,有六个答案,没有任何有用、正确或可操作的建议。忽略它们。

我已将错误范围缩小到特定代码段,如果没有运行此代码,应用程序不会冻结

很好,这是诊断问题的第一步。 走得更远。究竟这里的哪些方法是具有高延迟的?我的猜测是ReceiveRead,但有几个可能的候选者,而且可能不止一个。

确定每种高延迟方法后,确定它是否因为使用 CPU 或等待 IO 而变慢。

如果它因为使用 CPU 而变慢,您要做的就是将该操作移到另一个 CPU 上并等待结果。您可以按照其他答案的建议这样做:使用 Task.Run 将工作移至后台线程,然后使用 await 结果。

如果它因为等待 IO 而变慢,那么除非别无选择,否则不要将操作移至后台线程。后台工作 API 旨在将工作卸载到其他 CPU,并且 IO 不是 CPU 绑定的工作。您在这里要做的是使用异步 IO 操作。如果拦截器是Receive,比如说,那么使用ReceiveAsyncawait 生成Task

您还应该将您的方法设为async 并返回一个Task,然后由其调用者等待,依此类推,直到事件处理程序启动整个过程。

继续执行此过程,直到您确定方法中需要超过 30 毫秒的每个操作,并使其在工作线程上异步(如果 CPU 受限)或使用异步 IO 方法(如果 IO 受限)。你的 UI 应该永远不会有超过 30 毫秒的延迟。

说到事件处理程序...

此代码是用户如何接收来自服务器的请求。所以它在一个定时器中每 100 毫秒运行一次。

这听起来非常非常错误。首先,“收到来自服务器的请求”?服务器代表客户端进行请求,反之亦然。

其次,这听起来像是处理问题的错误方法。

如果您正确异步此代码,则不需要计时器来不断轮询套接字。您应该简单地异步读取,并且读取成功并回调您,或者超时。

另外,这里没有循环。您说您每 100 毫秒运行一次此代码,并且您在这里读取的最大值为 2K,所以无论您的网络连接有多饱和,您每秒读取的最大值为 20K,对吧?你觉得这对吗?因为这对我来说似乎是错误的。

【讨论】:

喜欢“异步”:D 我使用的是 I7700k 所以我不认为它是 CPU。该应用程序并不慢,它只是冻结且无法使用,我与客户端同时运行服务器并且服务器运行完美。客户端在计时器上检查是否已从服务器接收到请求。因此服务器可能会询问您的计算机名称是什么。客户端将接收该请求并将数据发送回服务器。 我想我明白你的意思。与其尝试不断循环直到收到响应,不如使用一些方法让客户端知道何时收到了请求。 @JeremyGriffin:不,我的意思是你异步接收,当数据存在时接收会回调你。 @JeremyGriffin:不要思考知道。使用您可以使用的工具来分析程序并找出 UI 线程阻塞超过 30 毫秒的位置,然后将其归类为 CPU 或 IO。 从知识和理解的角度工作,而不是猜测和希望【参考方案2】:

您可以使用BackGroundWorkerasync/await 都可以解决冻结UI的问题。

下面给出的代码是例如你可以用谷歌搜索它,使用其中一个可以解决你的问题


由于存在执行 IO 调用的操作,并且在带有 **async 的较新框架方法中,建议使用 async/awiat

async/await 方式(新方式 - .NET 4.5 for 4.0 版本你会发现 nuget)

private async void Button_Click(object sender, RoutedEventArgs 

      var task = Task.Factory.StartNew( () => longrunnincode());
      var items = await task;
      //code after task get completed 

后台工作人员(旧方式,但仍然支持并在许多旧应用程序中使用,因为 async/await 是在 .NET 4.5 中交付的 4.0 版本,你会发现 nuget)

private BackgroundWorker backgroundWorker1;
this.backgroundWorker1 = new BackgroundWorker(); 
this.backgroundWorker1.DoWork += new 
        DoWorkEventHandler(this.backgroundWorker1_DoWork);

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)

    BackgroundProcessLogicMethod();


private void backgroundWorker1_RunWorkerCompleted(object sender,

    RunWorkerCompletedEventArgs e)

  if (e.Error != null) MessageBox.Show(e.Error.Message);
    else MessageBox.Show(e.Result.ToString());
 

private void StartButton_Click(object sender, EventArgs e)


    // Start BackgroundWorker
    backgroundWorker1.RunWorkerAsync(2000);


除非没有其他办法,否则不要将后台工作人员用于 IO。直接调用异步 IO API。

【讨论】:

除非没有其他办法,否则不要使用后台工作人员进行 IO。直接调用异步 IO API。 @EricLippert - 谢谢先生,我刚刚在回答中添加了您的评论,a 你为什么要做你所说的不应该做的事? @Servy - 道歉,但没有让你..我建议两种方式,因为我使用了两种方式,后台工作程序我在没有异步/等待时在应用程序中使用,一旦我得到我在我的新应用 您建议了两种创建后台线程来执行 IO 工作的解决方案,以及在不创建/使用后台线程的情况下实际执行 IO 工作的零解决方案,这是正确的解决方案。使用哪种语法创建后台线程只是个人喜好,不影响答案。【参考方案3】:

所以为它制定了一些解决方法,没有使用异步但是我最终为这个任务创建了一个不同的线程。

新线程让我使用了一个 while(true) 循环来永远循环接收响应。

private void btnConnect_Click(object sender, EventArgs e)
    
        ConnectToServer();
        Thread timerThread = new Thread(StartTimer);
        timerThread.Start();
        //StartTimer();
        //timerResponse.Enabled = true;

    
    private static void StartTimer()
    
        while (true)
        
            ReceiveResponse();
        
    

这对我需要做的事情来说已经足够好了。

感谢大家的帮助!

【讨论】:

【参考方案4】:

在什么上下文中运行此代码更为重要。您可能正在 UI 线程上运行它。您应该将繁重的工作推迟到另一个工作线程或使方法异步。

【讨论】:

【参考方案5】:

这是 GUI 的常见问题。活动应该尽快结束。当一个事件没有返回时,没有其他事件可以运行。甚至不会执行使用最后更改更新 UI 的代码。并且所有 Windows 可以告诉用户的是程序“没有响应”(因为它还没有确认最后一个输入 Windows 发送它的方式)。

您需要做的是将长期运行的操作转移到某种形式的多任务处理中。有很多方法可以做到这一点,有些涉及多个线程,有些则不涉及。在 GUI 环境中开始多任务处理时(这是理想的情况),我会使用 BackgroundWorker。他不是您希望在生产性代码中/以后使用的东西,但它是一个很好的初学者工具来学习多任务处理的特性(竞争条件、异常处理、回调)。

由于通常不知道网络操作受 CPU 限制,从长远来看,您可能希望采用多任务的无线程方式。

【讨论】:

【参考方案6】:

听起来很可能是计时器。如果您使用Thread.Sleep() 方法,这肯定是问题所在。使用此函数会锁定当前线程,并且由于您的函数没有与您的 UI 线程异步运行,因此 UI 也会冻结。

要解决这个问题,您可以简单地将您提供的代码添加到异步任务中,并使用Task.Delay() 方法确保该函数仅每 100 毫秒执行一次。

更多信息请参见this question。

【讨论】:

代码中没有 Thread.Sleep(),我确保在发布之前删除了所有这些代码。 啊,好吧,我相信 C# Windows.Forms.Timer 类也可以在您的 UI 线程上运行。这是您使用的计时器吗? 是的,我正在使用默认的 winforms 计时器 会是这个问题吗? 有可能,您可以尝试从我添加的链接中实施async Task 解决方案。您将从主线程中的循环调用任务,并可以在异步方法的顶部添加 Task.Delay(100) 以确保它不会持续运行?

以上是关于如何阻止我的 UI 冻结?的主要内容,如果未能解决你的问题,请参考以下文章

Locator.geocode(...) 在 OS 5 设备(Blackberry Java)中冻结/阻止 UI

当我进行 api 调用时如何阻止 winforms 应用程序冻结

加载或倾倒泡菜时如何阻止动画 QCursor 冻结?

如何使用 QProcess 避免 waitForStarted 以阻止 GUI 冻结?

长轮询冻结浏览器并阻止其他 ajax 请求

Android:尽管使用了 AsyncTask,但主线程被阻止