在后台运行无限操作而不影响主线程的最佳方法?

Posted

技术标签:

【中文标题】在后台运行无限操作而不影响主线程的最佳方法?【英文标题】:Optimal way to run an infinite operation in the background without affecting the main thread? 【发布时间】:2021-09-20 18:15:49 【问题描述】:

我正在开发一个可以跟踪某些 GPU 参数的小型应用程序。我目前正在使用 5 个后台工作人员来跟踪 5 个不同的参数,这些操作一直运行到我的应用程序关闭为止。我知道这可能不是一个好方法。什么是在后台监控这些参数而不必为每个参数创建工作器的好方法?

编辑:恢复到我现在提出的原始问题,问题已重新打开。

仅监测温度的测试文件。

using NvAPIWrapper.GPU;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

namespace TestForm

    public partial class Form1 : Form
    
        private PhysicalGPU[] gpus = PhysicalGPU.GetPhysicalGPUs();

        public Form1()
        
            InitializeComponent();
            GPUTemperature();
        

        private void GPUTemperature()
        
            backgroundWorker1.RunWorkerAsync();
        

        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        
            while (!backgroundWorker1.CancellationPending)
            
                foreach (var gpu in gpus)
                
                    foreach (var sensor in gpu.ThermalInformation.ThermalSensors)
                    
                        backgroundWorker1.ReportProgress(sensor.CurrentTemperature);
                        Thread.Sleep(500);
                    
                
            
        

        private void backgroundWorker1_ProgressChanged(object sender,
            ProgressChangedEventArgs e)
        
            temperature.Text = e.ProgressPercentage.ToString();
        
    

【问题讨论】:

我投票决定将此问题作为题外话结束,因为有关 代码优化 的问题是题外话。请参阅 Can I post questions about optimizing code on Stack Overflow?Is it okay to ask for code optimization help? 了解更多信息。 我的首选方法是使用this 答案中的PeriodicAsync 方法。与使用BackgroundWorker 相比,它的优势在于它不会阻塞ThreadPool 线程。提供的操作在 UI 线程上调用,所以如果我必须使用 Task.RunProgress<int>,如图所示 here。与使用 System.Timers.Timer 相比,它的优势在于它不可重入。 我投票重新提出这个问题,因为 1)关于代码优化的问题是 *** 上的 on topic 和 2)这个问题不是关于代码优化,而是关于如何避免已知的技术浪费。不阻塞应用程序的ThreadPool 线程不是奢侈品,而是必需品。 我同意@TheodorZoulias,这个问题似乎是主题,CodeReview 不是处理此类问题的唯一站点代表。还有一个小的调试问题,因为e.ProgressPercentage 用于ReportProgressProgressChanged,这显然是错误的类型:应该使用ReportProgress(Int32, Object) 重载,以防BackgroundWorker 仍然是首选工具。一个(单)线程的 Timer 可能就足够了。以不会导致重叠事件的方式处理。 啊,我明白了。 Thread.Sleep(500); 在内部 foreach 循环中,而不是在外部 while 循环中。这不会导致更新缓慢且不一致吗?如果每半秒更新一次所有 GPU 的所有传感器会不会更好? 【参考方案1】:

在获得 cmets 的帮助后,我能够解决这个问题。这是我的最终工作代码。

using NVIDIAGPU.GPUClock;
using NVIDIAGPU.GPUFan;
using NVIDIAGPU.Memory;
using NVIDIAGPU.Thermals;
using NVIDIAGPU.Usage;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

namespace SysMonitor

    public partial class Form1 : Form
    
        private int[] sensorValues = new int[5];

        public Form1()
        
            InitializeComponent();

            StartWorkers();
        

        /// <summary>
        /// Store sensor parameters.
        /// </summary>
        public int[] SensorValues  get => sensorValues; set => sensorValues = value; 

        private void StartWorkers()
        
            thermalsWorker.RunWorkerAsync();
        

        #region ThermalWorker

        private void thermalsWorker_DoWork(object sender, DoWorkEventArgs e)
        
            while (!thermalsWorker.CancellationPending)
            
                // Assign array values.
                SensorValues[0] = GPUThermals.CurrentTemeperature;
                SensorValues[1] = GPUUsage.CurrentUsage;
                SensorValues[2] = (int)GPUClock.CurrentGPUClock;
                SensorValues[3] = (int)GPUMemory.CurrentMemoryClock;
                SensorValues[4] = GPUFan.CurrentFanRPM;

                // Pass the SensorValues array to the userstate parameter.
                thermalsWorker.ReportProgress(0, SensorValues);
                Thread.Sleep(500);
            
        

        private void backgroundWorker1_ProgressChanged(object sender,
            ProgressChangedEventArgs e)
        
            // Cast user state to array of int and assign values.
            int[] result = (int[])e.UserState;
            gpuTemperatureValue.Text = result[0].ToString() + " °C";
            gpuUsageValue.Text = result[1].ToString() + " %";
            gpuClockValue.Text = result[2].ToString() + " MHz";
            gpuMemoryValue.Text = result[3].ToString() + " MHz";
            gpuFanRPMValue.Text = result[4].ToString() + " RPM";

            
        
        #endregion ThermalWorker
    

【讨论】:

【参考方案2】:

我对所有现有答案都有疑问,因为它们在基本的关注点分离上都失败了。

但是,要解决此问题,我们需要重新构建问题。在这种情况下,我们不想“运行操作”,而是想“观察/订阅”GPU 状态。

在这种情况下,我更喜欢观察而不是 sub/pub,因为工作已经完成,你真的想知道是否有人在听你的树倒在森林里。

因此,这里是 RX.Net 实现的代码。

public class GpuMonitor

    private IObservable<GpuState> _gpuStateObservable;

    public IObservable<GpuState> GpuStateObservable => _gpuStateObservable;

    public GpuMonitor()
    
        _gpuStateObservable = Observable.Create<GpuState>(async (observer, cancellationToken) => 
        
            while(!cancellationToken.IsCancellationRequested)
            
                 await Task.Delay(1000, cancellationToken);
                 var result = ....;
                 observer.OnNext(result);
            
        )
            .SubscribeOn(TaskPoolScheduler.Default)
            .Publish()
            .RefCount();
    

然后去消费。

public class Form1

    private IDisposable _gpuMonitoringSubscription;


    public Form1(GpuMonitor gpuMon)
    
        InitializeComponent();
        _gpuMonitoringSubscription = gpuMon.GpuStateObservable
                .ObserveOn(SynchronizationContext.Current)
                .Susbscribe(state => 
                     gpuUsageValue.Text = $"state.Usage %";
                     //etc
                );
    

    protected override void Dispose(bool disposing)
    
        _gpuMonitoringSubscription.Dispose();
        base.Dispose(disposing);
    


这里的好处是你可以在多个地方重用这个组件,使用不同的线程等等。

【讨论】:

在将Observable.Create 与异步委托一起使用时要小心,然后在没有onError 处理程序的情况下订阅。在这种情况下,有一个讨厌的异常吞咽bug 可能会咬你。如果要使用 Rx 进行循环,最好使用 robust Repeat 运算符。【参考方案3】:

我的建议是废弃technologically obsoleteBackgroundWorker,而改用异步循环。传感器值的读取可以通过使用Task.Run 方法卸载到ThreadPool 线程,并且可以通过等待Task.Delay 任务来施加每次迭代之间的空闲时间:

public Form1()

    InitializeComponent();
    StartMonitoringSensors();


async void StartMonitoringSensors()

    while (true)
    
        var delayTask = Task.Delay(500);
        var (temperature, usage, gpuClock, memory, fan) = await Task.Run(() =>
        
            return
            (
                GPUThermals.CurrentTemperature,
                GPUUsage.CurrentUsage,
                GPUClock.CurrentGPUClock,
                GPUMemory.CurrentMemoryClock,
                GPUFan.CurrentFanRPM
            );
        );
        gpuTemperatureValue.Text = $"temperature °C";
        gpuUsageValue.Text = $"usage %";
        gpuClockValue.Text = $"gpuClock MHz";
        gpuMemoryValue.Text = $"memory MHz";
        gpuFanRPMValue.Text = $"fan RPM";
        await delayTask;
    

你用这种方法得到了什么:

    在读取传感器之间的空闲期间,ThreadPool 线程未被阻塞。如果您的应用程序更复杂,并且大量使用ThreadPool,这一事实将会产生影响。但是对于像这样的简单应用程序,除了显示传感器值之外什么都不做,这种好处主要是学术性的。 ThreadPool 线程在空闲期间将无事可做,无论如何都会处于空闲状态。无论如何,尽可能避免不必要地阻塞线程是一个好习惯。

    您会获得传递给 UI 线程的强类型传感器值。无需从object 类型转换,也无需将所有值转换为ints。

    读取GPUThermalsGPUUsage 等属性引发的任何异常都将在 UI 线程上重新引发。您的应用程序不会只是停止工作,而不给出任何错误发生的迹象。这就是在StartMonitoringSensors 方法的签名中选择async void 的原因。 Async void should be avoided in general,但这是一个例外情况,async void 优于 async Task

    您每 500 毫秒获得一次传感器值的一致更新,因为读取传感器值所需的时间不会添加到空闲期。这是因为 Task.Delay(500) 任务是在读取值之前创建的,然后是 awaited。

【讨论】:

接受这个作为答案,因为它显示了西奥多提到的更现代的方法。我现在对这两种方法如何工作有了一个好主意,谢谢大家。

以上是关于在后台运行无限操作而不影响主线程的最佳方法?的主要内容,如果未能解决你的问题,请参考以下文章

iOS 在后台保存主线程 NSManagedObjectContext 更改

在 Grails 中实现后台服务的最佳方式

.net 执行一个查询大量数据的方法 怎么用线程实现让它后台运行

Android Service解析

在后台和主线程中使用托管对象上下文

关于前台主键输入错误对后台hibernate方法的影响