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

Optimal way to run an infinite operation in the background without affecting the main thread?

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

编辑:回到原来的问题,现在问题已重新打开。

只监测温度的测试文件。

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();
        }
    }
}

在评论中得到一些帮助后,我能够解决问题。这是我的最终工作代码。

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
    }
}

我的建议是取消 technologically obsolete BackgroundWorker, and use instead an asynchronous loop. The reading of the sensor values can be offloaded to a ThreadPool thread by using the Task.Run method, and the idle period between each iteration can be imposed by awaiting a 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;
    }
}

您通过这种方法得到的结果:

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

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

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

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

我对所有现有答案都有疑问,因为它们都无法实现基本的关注点分离。

但是,要解决这个问题,我们需要重新设计问题。在这种情况下,我们不想“运行一个操作”,而是想“observer/subscribe”到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);
    }

}

这里的优点是,您可以在多个地方、使用不同的线程等重用该组件。