C# WPF Threading
我一直在遵循 WPF 线程模型指南来创建一个小应用程序,该应用程序将监视和显示当前和峰值 CPU 使用率。但是,当我在事件处理程序中更新我的当前 CPU 和峰值 CPU 时,我的窗口中的数字根本不会改变。调试时,我可以看到文本字段确实发生了变化,但没有在窗口中更新。
我听说构建这样的应用程序是不好的做法,应该改用 MVVM 方法。事实上,一些人对我能够在没有运行时异常的情况下运行它感到惊讶。无论如何,我想从第一个链接中找出代码示例/指南。
让我知道你的想法!
这是我的 xaml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <Window x:Class="UsagePeak2.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="CPU Peak" Height="75" Width="260"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center"> <Button Content="Start" Click="StartOrStop" Name="startStopButton" Margin="5,0,5,0" /> <TextBlock Margin="10,5,0,0">Peak:</TextBlock> <TextBlock Name="CPUPeak" Margin="4,5,0,0">0</TextBlock> <TextBlock Margin="10,5,0,0">Current:</TextBlock> <TextBlock Name="CurrentCPUPeak" Margin="4,5,0,0">0</TextBlock> </StackPanel> |
这是我的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | public partial class MainWindow : Window { public delegate void NextCPUPeakDelegate(); double thisCPUPeak = 0; private bool continueCalculating = false; PerformanceCounter cpuCounter; public MainWindow() : base() { InitializeComponent(); } private void StartOrStop(object sender, EventArgs e) { if (continueCalculating) { continueCalculating = false; startStopButton.Content ="Resume"; } else { continueCalculating = true; startStopButton.Content ="Stop"; startStopButton.Dispatcher.BeginInvoke( DispatcherPriority.Normal, new NextCPUPeakDelegate(GetNextPeak)); //GetNextPeak(); } } private void GetNextPeak() { cpuCounter = new PerformanceCounter("Processor","% Processor Time","_Total"); double currentValue = cpuCounter.NextValue(); CurrentCPUPeak.Text = Convert.ToDouble(currentValue).ToString(); if (currentValue > thisCPUPeak) { thisCPUPeak = currentValue; CPUPeak.Text = thisCPUPeak.ToString(); } if (continueCalculating) { startStopButton.Dispatcher.BeginInvoke( System.Windows.Threading.DispatcherPriority.SystemIdle, new NextCPUPeakDelegate(this.GetNextPeak)); } } } |
这里有一些问题。首先,您正在主线程上工作,但您正在以一种非常迂回的方式进行工作。正是这里的这一行让您的代码具有响应性:
1 2 3 | startStopButton.Dispatcher.BeginInvoke( System.Windows.Threading.DispatcherPriority.SystemIdle, new NextCPUPeakDelegate(this.GetNextPeak)); |
来自
Executes the specified delegate asynchronously at the specified priority on the thread the Dispatcher is associated with.
即使您以很高的速率发送此消息,您也会在 UI 线程上排队工作,方法是让回帖回到消息循环中发生在后台线程上。这就是让您的 UI 完全响应的原因。
也就是说,你想像这样重组你的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | private Task GetNextPeakAsync(CancellationToken token) { // Start in a new task. return Task.Factory.StartNew(() => { // Store the counter outside of the loop. var cpuCounter = new PerformanceCounter("Processor","% Processor Time","_Total"); // Cycle while there is no cancellation. while (!token.IsCancellationRequested) { // Wait before getting the next value. Thread.Sleep(1000); // Get the next value. double currentValue = cpuCounter.NextValue(); if (currentValue > thisCPUPeak) { thisCPUPeak = currentValue; } // The action to perform. Action<double, double> a = (cv, p) => { CurrentCPUPeak.Text = cv.ToString(); CPUPeak.Text = p.ToString(); }; startStopButton.Dispatcher.Invoke(a, new object[] { currentValue, thisCPUPeak }); } }, TaskCreationOptions.LongRunning); } |
以上注意事项:
-
根据 Dylan 的回答,对
PerformanceCounter 类上的NextValue 方法的调用必须有一段时间过期才能开始传递值。 -
CancellationToken 结构用于指示是否应该停止操作。这将驱动将在后台连续运行的循环。
-
您的方法现在返回一个代表背景信息的
Task 类。 -
不需要围绕
thisCPUPeak 值进行同步,因为单个后台线程是唯一可以读取和写入的地方;对Dispatcher 类上的Invoke 方法的调用具有传递给它的currentValue 和thisCPUPeak 值的副本。如果您想在任何其他线程(包括 UI 线程)中访问thisCPUPeak 值,则需要同步对该值的访问(很可能通过lock 语句)。
现在,您还必须在类级别上保留
1 2 | private Task monitorTask = null; private CancellationTokenSource cancellationTokenSource = null; |
然后改变你的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | private void StartOrStop(object sender, EventArgs e) { // If there is a task, then stop it. if (task != null) { // Dispose of the source when done. using (cancellationTokenSource) { // Cancel. cancellationTokenSource.Cancel(); } // Set values to null. task = null; cancellationTokenSource = null; // Update UI. startStopButton.Content ="Resume"; } else { // Update UI. startStopButton.Content ="Stop"; // Create the cancellation token source, and // pass the token in when starting the task. cancellationTokenSource = new CancellationTokenSource(); task = GetNextPeakAsync(cancellationTokenSource.Token); } } |
请注意,它不是检查标志,而是检查
取消现有循环
您没有看到 UI 更新,因为您没有正确使用性能计数器。第一次查询处理器时间性能计数器时,它将始终为 0。请参阅此问题。
1 2 3 4 5 6 7 | cpuCounter = new PerformanceCounter("Processor","% Processor Time","_Total"); double currentValue = cpuCounter.NextValue(); Thread.Sleep(1000); currentValue = cpuCounter.NextValue(); |
这样简单的事情就可以解决问题,但您可能希望开发一个更强大的解决方案,同时考虑到上面评论中的一些评论。