如何阻止我的 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】:
到目前为止,有六个答案,没有任何有用、正确或可操作的建议。忽略它们。
我已将错误范围缩小到特定代码段,如果没有运行此代码,应用程序不会冻结
很好,这是诊断问题的第一步。 走得更远。究竟这里的哪些方法是具有高延迟的?我的猜测是Receive
或Read
,但有几个可能的候选者,而且可能不止一个。
确定每种高延迟方法后,确定它是否因为使用 CPU 或等待 IO 而变慢。
如果它因为使用 CPU 而变慢,您要做的就是将该操作移到另一个 CPU 上并等待结果。您可以按照其他答案的建议这样做:使用 Task.Run
将工作移至后台线程,然后使用 await
结果。
如果它因为等待 IO 而变慢,那么除非别无选择,否则不要将操作移至后台线程。后台工作 API 旨在将工作卸载到其他 CPU,并且 IO 不是 CPU 绑定的工作。您在这里要做的是使用异步 IO 操作。如果拦截器是Receive
,比如说,那么使用ReceiveAsync
和await
生成Task
。
您还应该将您的方法设为async
并返回一个Task
,然后由其调用者等待,依此类推,直到事件处理程序启动整个过程。
继续执行此过程,直到您确定方法中需要超过 30 毫秒的每个操作,并使其在工作线程上异步(如果 CPU 受限)或使用异步 IO 方法(如果 IO 受限)。你的 UI 应该永远不会有超过 30 毫秒的延迟。
说到事件处理程序...
此代码是用户如何接收来自服务器的请求。所以它在一个定时器中每 100 毫秒运行一次。
这听起来非常非常错误。首先,“收到来自服务器的请求”?服务器代表客户端进行请求,反之亦然。
其次,这听起来像是处理问题的错误方法。
如果您正确异步此代码,则不需要计时器来不断轮询套接字。您应该简单地异步读取,并且读取成功并回调您,或者超时。
另外,这里没有循环。您说您每 100 毫秒运行一次此代码,并且您在这里读取的最大值为 2K,所以无论您的网络连接有多饱和,您每秒读取的最大值为 20K,对吧?你觉得这对吗?因为这对我来说似乎是错误的。
【讨论】:
喜欢“异步”:D 我使用的是 I7700k 所以我不认为它是 CPU。该应用程序并不慢,它只是冻结且无法使用,我与客户端同时运行服务器并且服务器运行完美。客户端在计时器上检查是否已从服务器接收到请求。因此服务器可能会询问您的计算机名称是什么。客户端将接收该请求并将数据发送回服务器。 我想我明白你的意思。与其尝试不断循环直到收到响应,不如使用一些方法让客户端知道何时收到了请求。 @JeremyGriffin:不,我的意思是你异步接收,当数据存在时接收会回调你。 @JeremyGriffin:不要思考。 知道。使用您可以使用的工具来分析程序并找出 UI 线程阻塞超过 30 毫秒的位置,然后将其归类为 CPU 或 IO。 从知识和理解的角度工作,而不是猜测和希望。【参考方案2】:您可以使用BackGroundWorker
或async/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 应用程序冻结