65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 多线程,一对一 - 一种简单的多线程实现方式

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.52/5 (12投票s)

2007 年 6 月 9 日

CPOL

4分钟阅读

viewsIcon

33730

downloadIcon

694

如何在 .NET 中使用 C# 进行多线程编程,并从工作线程更新 UI

Screenshot - threadding_test_screen01.jpg

Screenshot - threadding_test_screen02.jpg

引言

正如您在截图中所看到的,我有 3 个后台线程,每个线程都在执行一个耗时的操作,但所有 3 个线程都需要显示它们的进度,所以我会简单地解释如何做到这一点。

背景

我在 CodeProject 上阅读文章已经有一段时间了(实际上是 3 年了),但我一直难以找到一篇关于多线程这个棘手主题的优秀文章。而且,因为我过去是 Win32 开发者,所以我知道多线程有多么痛苦。同时,作为一名 .NET 开发者,我知道框架在使多线程变得容易方面做得非常出色,但对初学者来说并不容易。所以我决定将这篇文章作为初学者学习 .NET 中多线程的起点,特别是关于从另一个线程更新 UI(用户界面),因为这是初学者最难做到的事情。

使用代码

我决定尽可能地简化它,所以项目只是一个在 Visual Studio 2005 中开发的 Windows Forms 项目,并且只包含一个显示进度的窗体。

我首先放置了进度条和按钮,当然也给它们起了名字,以便我能更轻松地工作。

接下来,我需要定义一个委托,用于在后台执行一些处理的工作线程,它看起来像这样:

private delegate void DoOperationDelegate(int seconds, ProgressBar pBar, Button button);

该委托接受 3 个参数:

  • int seconds:表示操作延迟的毫秒数。
  • ProgressBar pBar:关联的进度条,用于显示当前操作的进度。
  • Button button:操作完成后启用的按钮。

所以我们现在对委托没有问题了,我们需要一个执行实际工作的函数,它看起来像那个委托,下面是它的代码:

 
private void DoOperation(int seconds, ProgressBar pBar, Button button)
{
    for (int i = 0; i <= 100; i++)
    {
        System.Threading.Thread.Sleep(seconds / 100);
        UpdateUI(button, pBar, i);
    }
}

它非常自明,但让我们稍作讨论。正如我们所见,该方法有一个从 0 到 100 的 for 循环,这对应于进度条的默认值范围。所以,我正在执行一个 100 步的操作,在每一步中,我实际上什么都不做,只是通过 `Thread.Sleep(some time)` 来暂停,这个 `some time` 是操作的总毫秒数除以 100 步。但是,你可以在这里执行实际的耗时工作,而不用让线程休眠。

在这个休眠操作之后,我调用了一个非常重要的方法,即 `UpdateUI` 方法,它实际显示了进度,同时传递了进度条、按钮和实际的进度值。

现在让我们检查代码的另一部分,即 UpdateUI 委托。

私有的

delegate void UpdateUIDelegate(Button button, ProgressBar pBar, int value);

该委托接受三个参数:

  • Button button:操作完成后启用的按钮。
  • ProgressBar pBar:关联的进度条,用于显示当前操作的进度。
  • int value:操作进度的当前值(0 - 100)。

我们使用以下代码实现了该委托:

private void UpdateUI(Button button, ProgressBar pBar, int value)
{
    if (this.InvokeRequired)
    {
        UpdateUIDelegate d = UpdateUI;
        this.Invoke(d, new object[] {button, pBar, value });
        return;
    }

    
    if (value >= 100)
    {
        button.Enabled = true;
        pBar.Value = 100;
    }
    else
    {
        pBar.Value = value;
    }
}

正如您所见,这里是实际的线程同步工作。所以,在第一块代码中,我们检查了窗体的 `InvokeRequired` 属性,以查看该方法是否是从另一个线程调用的。如果返回 true,则表示我们正在从另一个线程运行,所以我们需要告诉 UI 线程去执行工作,而不是我们当前线程。这通过调用 `this.Invoke` 来实现,因为窗体(`this`)将在其线程上执行委托的方法,并且我们以与接收到的参数相同的顺序传递了相同的参数给委托。

所以,当这种情况发生时,窗体的线程将执行我们的方法,当我们检查 `InvokeRequired` 时,我们会发现它是 `false`,然后我们可以安全地继续更新我们的 UI。

到目前为止,我们只缺少一件事,那就是从新线程启动处理的代码。

private void startButton1_Click(object sender, EventArgs e)
{
    startButton1.Enabled = false;
    DoOperationDelegate d = new DoOperationDelegate(DoOperation);
    d.BeginInvoke(1000, progressBar1, startButton1, null, null);
}

在这段代码中,您可以看到第一个进程按钮的点击事件处理程序。实际上,这三个按钮的事件处理程序都是相同的,除了它们传递给委托的参数不同。在创建了委托的新实例后,它们会调用 `BeginInvoke`,这将启动一个新线程并使用传递的参数执行引用的方法。但是,有一点需要大家知道,`BeginInvoke` 方法还有两个额外的参数,我将它们传递为 null,因为我不需要它们,而且它们超出了我们的初学者范围。

关注点

我写这篇文章的目的是与大家一起学习安全的线程编程,我希望我做得很好。我将尝试发布其他高级示例,但我认为目前这些内容已经足够了。玩得开心,感谢阅读这篇文章!

历史

2007 年 6 月 9 日:首次发布。

© . All rights reserved.