C# WPF 线程

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

来自 BeginInvoke 方法文档(重点是我的):

Executes the specified delegate asynchronously at the specified priority on the thread the Dispatcher is associated with.

即使您以很高的速率发送此消息,您也会在 UI 线程上排队工作,方法是让回帖回到消息循环中发生在后台线程上。这就是让您的 UI 完全响应的原因。

也就是说,你想像这样重组你的 GetNextPeak 方法:

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 方法的调用具有传递给它的 currentValuethisCPUPeak 值的副本。如果您想在任何其他线程(包括 UI 线程)中访问 thisCPUPeak 值,则需要同步对该值的访问(很可能通过 lock 语句)。

现在,您还必须在类级别上保留 Task 并引用 CancellationTokenSource(生成 CancellationToken):

1
2
private Task monitorTask = null;
private CancellationTokenSource cancellationTokenSource = null;

然后改变你的 StartOrStop 方法来调用任务

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

请注意,它不是检查标志,而是检查 Task 是否已经在运行;如果没有 Task 则启动循环,否则,使用 CancellationTokenSource.

取消现有循环


您没有看到 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();

这样简单的事情就可以解决问题,但您可能希望开发一个更强大的解决方案,同时考虑到上面评论中的一些评论。