Windows 窗体应用程序中的多线程调用?

Posted

技术标签:

【中文标题】Windows 窗体应用程序中的多线程调用?【英文标题】:Multi-threading calls in Windows Forms application? 【发布时间】:2010-02-28 22:40:40 【问题描述】:

我正在尝试使我的 C# 应用程序成为多线程的,因为有时我会收到一个异常,指出我以不安全的方式调用了一个线程。我以前从未在程序中做过任何多线程,所以如果我对这个问题听起来有点无知,请耐心等待。

我的程序概述是我想制作一个性能监控应用程序。这需要使用 C# 中的进程和性能计数器类来启动和监视应用程序的处理器时间,并将该数字发送回 UI。但是,在实际调用性能计数器的 nextValue 方法(由于计时器设置为每秒执行一次)的方法中,我有时会遇到上述异常,它会谈论以不安全的方式调用线程。

我附上了一些代码供您阅读。我知道这是一个耗时的问题,所以如果有人能就在哪里创建新线程以及如何以安全的方式调用它提供任何帮助,我将不胜感激。我尝试查看 MSDN 上的内容,但这让我有点困惑。

private void runBtn_Click(object sender, EventArgs e)

    // this is called when the user tells the program to launch the desired program and
    // monitor it's CPU usage.

    // sets up the process and performance counter
    m.runAndMonitorApplication();

    // Create a new timer that runs every second, and gets CPU readings.
    crntTimer = new System.Timers.Timer();
    crntTimer.Interval = 1000;
    crntTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent);
    crntTimer.Enabled = true;


private void OnTimedEvent(object source, ElapsedEventArgs e)

    // get the current processor time reading 
    float cpuReading = m.getCPUValue();

    // update the current cpu label
    crntreadingslbl.Text = cpuReading.ToString(); // 


// runs the application 
public void runAndMonitorApplication()

    p = new Process();
    p.StartInfo.UseShellExecute = true;
    p.StartInfo.CreateNoWindow = true;
    p.StartInfo.FileName = fileName;
    p.Start();

    pc = new System.Diagnostics.PerformanceCounter("Process",
                "% Processor Time",
                p.ProcessName,
                true);


// This returns the current percentage of CPU utilization for the process
public float getCPUValue()

    float usage = pc.NextValue();

    return usage;

【问题讨论】:

【参考方案1】:

查看 Jon Skeet 关于多线程的文章,尤其是 multi-threading winforms 上的页面。它应该可以解决您的问题。

基本上,您需要检查是否需要调用,然后在需要时执行调用。阅读本文后,您应该能够将 UI 更新代码重构为如下所示的块:

private void OnTimedEvent(object source, ElapsedEventArgs e)

    // get the current processor time reading 
    float cpuReading = m.getCPUValue();

    if (InvokeRequired)
    
        // We're not in the UI thread, so we need to call BeginInvoke
        BeginInvoke(new Action(() => crntreadingslbl.Text = cpuReading.ToString()));
        return;
    
    // Must be on the UI thread if we've got this far
    crntreadingslbl.Text = cpuReading.ToString();

在您的代码中,由于您使用的是计时器,因此需要调用。根据System.Timers.Timer的文档:

在 ThreadPool 线程上引发 Elapsed 事件。

这意味着您设置为 Timer 委托的 OnTimedEvent() 方法将在下一个可用的 ThreadPool 线程上执行,该线程绝对不会是您的 UI 线程。该文档还建议了解决此问题的另一种方法:

如果您将计时器与用户一起使用 界面元素,例如表单或 控制,分配表格或控制 包含 Timer 到 SynchronizingObject 属性,这样 事件被编组给用户 接口线程。

您可能会发现这条路线更容易,但我还没有尝试过。

【讨论】:

好的,这个和后台工作人员的评论看起来很有用;但据我了解,进程本身在 UI 线程上运行,但我必须创建一个单独的线程来收集和更新该进程的数据?一般来说,我如何才能知道在哪里创建一个单独的线程? 当计时器“关闭”时,计时器将在第一个可用的线程池线程上执行请求的 ElapsedEventHandler 委托。因此,无论您要求计时器做什么,都将发生在单独的线程上,而不是 UI 线程上。添加后台工作者只会在等式中引入另一个线程。【参考方案2】:

我认为你的问题是这一行:

crntreadingslbl.Text = cpuReading.ToString();

在 UI 线程之外运行。您无法在 UI 线程之外更新 UI 元素。您需要在 Window 上调用 Invoke 才能在 UI 线程上调用新方法。

说了这么多,为什么不使用 perfmon 呢?它是为目的而构建的。

【讨论】:

【参考方案3】:

BackGroundWorker 组件可能会对您有所帮助。它位于工具箱中,因此您可以拖动到表单中。

此组件公开一组事件以在不同于 UI 线程的线程中执行任务。您不必担心创建线程。

在后台运行的代码和 UI 控件之间的所有交互都必须通过事件处理程序来完成。

对于您的场景,您可以设置一个计时器以在特定时间间隔触发后台工作人员。

private void OnTimedEvent(object source, ElapsedEventArgs e)

    backgroundWorker.RunWorkerAsync();

然后您实现适当的事件处理程序来实际收集数据并更新 UI

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)

    // Collect performance data and update the UI   

【讨论】:

以上是关于Windows 窗体应用程序中的多线程调用?的主要内容,如果未能解决你的问题,请参考以下文章

C# Windows窗体应用程序自动执行(2021.6.4)

如何跨线程调用Windows窗体控件

C# winform 跨线程操作winform程序窗体

VB.net可以多线程控制同一个窗体及其控件吗

在退出之前跟踪 - 并正确结束 - C# - C++/CLI - C++ Windows 窗体应用程序中的本机和托管线程

nodejs中的fiber(纤程)库详解