记录一个跨线程更新UI控件失效的问题

165 阅读4分钟
Task.Run(async ()=>{
    While(!cts.IsIsCancellationRequested){
        await Task.Delay(200);
        var res = ModbusTcpNet.Write("100", 1).IsSuccess;
        if(res){
            this.Invoke(() => this.led_PlcState.On = true;);
        }
    }
})

这段代码运行时发现UI控件并没有更新,断点调试时,发现到this.Invoke这一步后,断点失效。 神奇的是,当我把Delay的延迟增加到1000以上时,控件可以正确的更新,断点也可以生效。

多方查询后,推测原因是:一个任务如果非常快速的完成(200ms),它可能会在当前程序的同步上下文中直接排队并立即执行,而不是将执行推送到线程池里的某一个线程上,以免不必要的资源开销,所以没有上下文切换。

所以在多线程操作中更新UI控件时,更好的方法是事先使用Control.InvokeRequired属性来判断是否需要跨线程调用。

//修改优化后的代码
Task.Run(async ()=>{
    While(!cts.IsIsCancellationRequested){
        await Task.Delay(200);
        var res = ModbusTcpNet.Write("100", 1).IsSuccess;
        if(res){
            UpdateUI(led_PlcState,()=> led_PlcState.On = true);
        }
    }
})
//安全的更新UI控件
privite void UpdateUI(Control controls,Action action){
    if(controls.InvokeRequired){
        controls.Invoke(action);
    }
    action();
}

此外还了解到一些额外的概念。

最小线程数的概念

.NET线程池是一个管理一组后台线程的系统,这些线程可以被用来执行异步操作或长时间运行的任务。为了优化性能,.NET线程池实现了一个机制来控制线程的数量,其中包括了最小线程数的概念。

线程池最小线程数

最小线程数指的是线程池中预先创建并保持空闲状态以准备处理工作项的线程数量。这包括用于所有类型的工作项(如I/O绑定、计算密集型任务等)的线程。每个CPU核心都有一个默认的最小线程数,这样当有多个任务需要并发执行时,线程池可以快速响应而不需要立即创建新的线程。

在应用程序启动时,线程池会根据系统的处理器数量和一些其他因素设置初始的最小线程数。对于多核处理器,这意味着线程池可能会为每个处理器核心分配至少一个线程,确保在多核环境中能充分利用硬件资源。

工作项的分配

当提交一个新的工作项给线程池时,如果当前有空闲的线程(即没有正在执行其他任务的线程),则该线程将被分配来执行新的工作项。如果没有空闲线程,并且当前线程数低于最大允许的线程数,那么线程池可能会创建新的线程来处理额外的工作负载。

然而,频繁地创建和销毁线程是昂贵的操作,因为它涉及到内存分配和上下文切换。为了避免这种开销,线程池不会总是立即创建新线程。相反,它会等待一段时间看看是否有现有线程变为可用。如果在这段时间内没有空闲线程出现,线程池最终会创建新的线程,但这个过程是受到限制的,以防止过多的线程消耗系统资源。

异步操作与快速完成

对于非常短时间延迟的异步操作,比如使用Task.Delay(200),如果这段时间足够短,线程池中的某个线程可能已经完成了之前分配给它的任务并且变为空闲。在这种情况下,这个空闲线程可以直接拾取新的工作项并执行它,而无需等待新的线程创建或者进行复杂的上下文切换。由于线程已经是存在的,所以它可以迅速开始执行新的任务,从而提高了效率。

此外,如果异步操作确实是在很短时间内完成的,线程池甚至可能安排它在调用线程上直接执行,特别是在UI应用程序中,以保证UI线程的响应性,同时避免不必要的线程切换开销。

设置最小线程数

开发者可以通过ThreadPool.SetMinThreads方法来调整线程池的最小线程数。不过,通常不建议手动调整这些值,除非你确切知道你在做什么,并且有明确的理由这样做。错误地配置线程池参数可能导致性能问题,例如饥饿(starvation)现象,其中某些工作项无法得到及时处理,因为线程被其他任务占用。

线程池的最小线程数有助于减少线程创建的频率,提高对短期或突发性工作负载的响应速度,同时也尽量减少了不必要的上下文切换,从而提升了整体性能。