在后台运行无限操作而不影响主线程的最佳方法?
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.Run
和 Progress<int>
,如图所示 here。与使用 System.Timers.Timer
相比,它的优势在于它不可重入。
我投票重新提出这个问题,因为 1)关于代码优化的问题是 *** 上的 on topic 和 2)这个问题不是关于代码优化,而是关于如何避免已知的技术浪费。不阻塞应用程序的ThreadPool
线程不是奢侈品,而是必需品。
我同意@TheodorZoulias,这个问题似乎是主题,CodeReview 不是处理此类问题的唯一站点代表。还有一个小的调试问题,因为e.ProgressPercentage
用于ReportProgress
和ProgressChanged
,这显然是错误的类型:应该使用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
类型转换,也无需将所有值转换为int
s。
读取GPUThermals
、GPUUsage
等属性引发的任何异常都将在 UI 线程上重新引发。您的应用程序不会只是停止工作,而不给出任何错误发生的迹象。这就是在StartMonitoringSensors
方法的签名中选择async void
的原因。 Async void should be avoided in general,但这是一个例外情况,async void
优于 async Task
。
您每 500 毫秒获得一次传感器值的一致更新,因为读取传感器值所需的时间不会添加到空闲期。这是因为 Task.Delay(500)
任务是在读取值之前创建的,然后是 await
ed。
【讨论】:
接受这个作为答案,因为它显示了西奥多提到的更现代的方法。我现在对这两种方法如何工作有了一个好主意,谢谢大家。以上是关于在后台运行无限操作而不影响主线程的最佳方法?的主要内容,如果未能解决你的问题,请参考以下文章
iOS 在后台保存主线程 NSManagedObjectContext 更改