Winform-耗时操作导致界面渲染滞后

原因:

某些耗时操作阻塞了主线程。

理解上述原因,需先搞清楚Winform线程机制。主要有以下2点特性:1.单线程模型;2.依赖消息循环。

1.单线程模型

Winform 默认是单线程。通常,所有的UI操作,包括控件更新、事件处理,都由主线程管理(也就是UI线程)。

任何在事件处理程序中运行的代码都会占用主线程。如果某个事件中有耗时很多的操作,就会阻塞线程。比如有时PictureBox.Image的显示会滞后。

下面代码中,pictureBox1的显示会有滞后。背后的工作原理是:

"pictureBox1.Image = image0"运行完成后,系统只是将 pictureBox1 的 Image 属性更新,但不会立即触发控件绘制。(实际的绘制操作是由消息循环处理的,在 WM_PAINT 消息中完成)

pictureBox1的属性设置之后,如果紧接着有耗时操作占用UI线程,就会导致WM_PAINT无法及时处理,所以不能及时绘制。

最佳解决办法:

避免在 UI 线程执行耗时任务。将耗时任务移到后台线程,可以确保 UI 线程始终空闲以处理消息循环,比如使用 Task.Run 或 async/await。

csharp 复制代码
private void button2_Click(object sender, EventArgs e)
{
    using (var session = new InferenceSession(_modelPath))
    {
        ......  // 无关代码

        Bitmap image0 = new Bitmap(_imagePath);
        pictureBox1.Image = image0;

        // 以下是某耗时操作
        DenseTensor<float> inputTensor = PreprocessImage(image0);
        ......
    }
}

补充一点:只有主线程可以访问或更新控件;如果要从其他线程访问、更新控件,要使用特定的线程间通讯,必须使用 Control.Invoke 或 Control.BeginInvoke 方法将操作封送到 UI 线程执行。如果在后台线程中得到某个变量,需要渲染到主界面,就需要用到Control.Invoke等方法。

2.依赖消息循环

UI线程依赖消息循环(Message Loop)处理用户输入、标点击等操作和系统消息。Application.Run 启动时,UI线程进入消息循环状态,不断从消息队列中提取消息并分发到对应控件处理。上述提到的线程阻塞实际上是阻塞了消息循环。

上述2个特性明白之后,就能理解原因中提到的阻塞了。如果不理解,可以尝试在pictureBox1.Image之后跟随一个简单的耗时操作帮助理解。