异步函数冻结 UI 线程

Posted

技术标签:

【中文标题】异步函数冻结 UI 线程【英文标题】:Async function freezes UI thread 【发布时间】:2021-12-24 16:29:20 【问题描述】:

我有一个异步函数,当我执行它时它仍然会冻结/滞后 UI 线程。这是我调用它的函数。

private void TcpListenerLogic(object sender, string e)
        
            Application.Current.Dispatcher.BeginInvoke((Action)async delegate 
                try
                
                    dynamic results = JsonConvert.DeserializeObject<dynamic>(e);
                    if (results.test_id != null)
                    
                        // Get properties for new anchor
                        string testInformation = await CommunicationCommands.getJsonFromURL(
                            "http://" + ServerIP + ":" + ServerPort + "/api/" + results.test_id);
                                        
                
                catch (Exception exception)
                
                    // Writing some Trace.WriteLine()'s
                
            );
           
        

这是冻结我的 UI 线程的异步函数

        public static async Task<string> getJsonFromURL(string url)
        
            
            try
            
                string returnString = null;
                using (System.Net.WebClient client = new System.Net.WebClient())
                
                    returnString = await client.DownloadStringTaskAsync(url);
                
                return returnString;
            
            catch (Exception ex)
            
                Debug.WriteLine(ex.ToString());
                return null;
            

        

我已经尝试让 TcpListenerLogic 中的所有内容在新的 Thread 中运行:

            new Thread(() =>
            
                Thread.CurrentThread.IsBackground = true;

            ).Start();

这导致整个 UI 完全冻结。我试图使TcpListenerLogic 异步并等待调度程序,这也使一切都永久冻结。我还尝试使TcpListenerLogic 异步并离开调度程序。调度程序只是在那里,因为我通常在那里有一些 UI 代码,我在测试时忽略了这些代码。

我已经在互联网上冒险了很长时间,但没有BackgroundWorkerThreadPool 或其他方法帮助我努力。 如果有人对这个特定问题有帮助,或者有资源可以提高我对 C# 中异步函数的理解,我将不胜感激。

编辑

应要求更深入地了解如何调用此事件处理程序。 我有 System.Net.Websocket,它连接到我正在使用的后端 API,并在他每次收到新数据时触发一个事件。为了保证套接字在打开时一直监听,有一个 while 循环来检查客户端状态:

        public event EventHandler<string> TcpReceived;
        public async void StartListener(string ip, int port, string path)
        
            try
            
                using (client = new ClientWebSocket())
                
                    try
                       // Connect to backend
                        Uri serverUri = new Uri("ws://" + ip + ":" + port.ToString() + path );
                        await client.ConnectAsync(serverUri, CancellationToken.None);
                    
                    catch (Exception ex)
                    
                        BackendSettings.IsConnected = false;
                        Debug.WriteLine("Error connecting TCP Socket: " + ex.ToString());
                    
                    state = client.State;
                    // Grab packages send in backend
                    while (client.State == WebSocketState.Open || client.State == WebSocketState.CloseSent)
                    
                        try
                        
                            // **Just formatting the received data until here and writing it into the "message" variable**//
                            TcpReceived(this, message);

                            // Close connection on command
                            if (result.MessageType == WebSocketMessageType.Close)
                            
                                Debug.WriteLine("Closing TCP Socket.");
                                shouldstayclosed = true;
                                await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
                                break;
                            
                            state = client.State;
                        
                        catch
                        
                            BackendSettings.IsConnected = false;
                            state = client.State;
                        
                    
                    state = client.State;
                
            
            catch (Exception ex)
            
                // Some error messages and settings handling
            
        

事件附加了一个处理程序:

TcpReceived += TcpListener_TcpReceived;

这就是Handler,它调用之前看到的“TcpListenereLogic”。

        private void TcpListener_TcpReceived(object sender, string e)
        
            TcpListenerLogic(sender, e);

            //App.Current.Dispatcher.BeginInvoke(new Action(() => 
            //      TcpListenerLogic(sender, e);
            //));

            //new Thread(() =>
            //
            //    Thread.CurrentThread.IsBackground = true;
            //    TcpListenerLogic(sender, e);
            //).Start();
        

我以前使用“TcpListenereLogic”作为处理程序,但我想尝试不同的方法来调用它。我还留在了注释掉的部分,以展示“TcpListenereLogic”的调用看起来如何。我所有的尝试都使用了所有提到的设置,遗憾的是没有任何结果。

【问题讨论】:

await 不会自动启动新任务,这就是您的 UI 仍然冻结的原因。使用Task.Run。你可能想通读this的答案。 TcpListenerLogic 方法是如何以及在哪里调用的? ^^ 是事件处理器吗? 我建议的第一件事是使用 WebClient。 并非所有异步方法都是非阻塞的,即使它们应该是。我还建议查看Task based asynchronous programming,了解在后台运行的现代方式。 【参考方案1】:

非常感谢@TheodorZoulias 帮助我找到解决问题的方法。

原来这不是异步函数本身,而是它被调用的频率。它每秒大约被调用约 120 次。 我的解决方案首先在新线程上调用 Listener 方法:

new Thread(() =>

    Thread.CurrentThread.IsBackground = true;
    MainWindow.tcpListener.StartListener(ip, portNumber, "/api/");
).Start();

为了限制每秒发生的调用量,我添加了一个调度程序计时器,它在我的事件用于调用后重置一个布尔值。

readonly System.Windows.Threading.DispatcherTimer packageIntervallTimer = 
                            new System.Windows.Threading.DispatcherTimer();
        bool readyForNewPackage = true;
        private void ReadyForPackage(object sender, EventArgs e)
        
            readyForNewPackage = true;
        

        public async void StartListener(string ip, int port, string path)
        
            packageIntervallTimer.Interval = TimeSpan.FromMilliseconds(50);
            packageIntervallTimer.Tick += (s, e) =>  Task.Run(() => ReadyForPackage(s, e)); ;
            packageIntervallTimer.Start();

然后我将 while 循环中的所有内容包装成一个基于布尔值的 if 条件,最重要的部分是在其中包含我的“事件 EventHandler TcpReceived”:

// Grab packages sent in backend
while (client.State == WebSocketState.Open || client.State == WebSocketState.CloseSent)

    if (readyForNewPackage == true)
    
        readyForNewPackage = false;
        try
        
           ....
           TcpReceived(this, message);
           ....
        
        catch
        
            ...
        
    

我将我的 TcpListenerLogic 添加到事件处理程序中:

TcpReceived += TcpListenerLogic;

我的 TcpListenerLogic 现在看起来像这样(名称已更改):

private async void TcpListenerLogic(object sender, string e)

    try
    
        dynamic results = JsonConvert.DeserializeObject<dynamic>(e);
        if (results.test_id != null)
        
            string testID = "";
            if (results.test_id is JValue jValueTestId)
            
                testID = jValueTestId.Value.ToString();
            
            else if (results.test_id is string)
            
                testID = results.test_id;
            

            // Get properties for new object
            string information = await CommunicationCommands.getJsonFromURL(
                "http://" + ServerIP + ":" + ServerPort + "/api/" + testID );
            if (information != null)
            
                await App.Current.Dispatcher.BeginInvoke(new Action(() =>
                
                    // Create object out of the json string
                    TestStatus testStatus = new TestStatus();
                    testStatus.Deserialize(information);
                    if (CommunicationCommands.isNameAlreadyInCollection(testStatus.name) == false)
                    
                        // Add new object to the list
                        CommunicationCommands.allFoundTests.Add(testStatus);
                    
                ));
            
    
    catch (Exception exception)
    
        ....
    


添加一个新线程来执行任何步骤都会导致问题,所以请记住,所有这些都使用在开头为“StartListener”创建的线程

【讨论】:

以上是关于异步函数冻结 UI 线程的主要内容,如果未能解决你的问题,请参考以下文章

当 GL 更新大于 15 Hz 时 Qt UI 冻结

如何将调色板图像从另一个线程生成到 WPF UI 线程?

在后台Android中进行繁重处理时UI冻结

如何在Android开发中用AsyncTask异步更新UI界面

为啥我的 pyqt 信号错误会冻结 ui,直到调用另一个 python 函数

Android实战----Android Retrofit是怎么将回调函数放到UI线程(主线程)中的(源码分析)