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






2.52/5 (12投票s)
如何在 .NET 中使用 C# 进行多线程编程,并从工作线程更新 UI
引言
正如您在截图中所看到的,我有 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 日:首次发布。