我想创建一个带有进度条和取消按钮的表单。我的程序中的代码将运行算法并更新进度条。我实现了一个可以工作的原型(如下)。我想知道这是否是正确的实现,或者是否有更好的方法来做到这一点。特别是,我想知道跨线程访问 UI 进度条是否存在任何问题。我过去曾见过代码可以(似乎)正确运行,但在调试器下运行时会引发跨线程异常。
这是我实现可取消进度条的基本形式。
public partial class CancellableProgressForm : Form
{
public CancellableProgressForm()
{
InitializeComponent();
}
public int PercentComplete
{
set
{
progressBar.Value = value;
}
}
private void buttonCancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
this.Close();
}
}
以及用于在带有运行按钮的表单上测试它的原型代码:
private void buttonRun_Click(object sender, EventArgs e)
{
var progressForm = new CancellableProgressForm();
var progress = new Progress<int>(percent =>
{
progressForm.PercentComplete = percent;
});
Task.Run(() =>
{
DoWork(progress);
progressForm.DialogResult = DialogResult.OK;
progressForm.Close();
});
DialogResult dr = progressForm.ShowDialog();
MessageBox.Show($"Result={dr}");
}
// Prototype code to "do some work" and update the progress bar on the progress form
private void DoWork(IProgress<int> progress)
{
for (int i = 1; i <= 100; ++i)
{
Thread.Sleep(100);
progress.Report(i);
}
}
正如我所说,无论有没有调试器,代码都可以正常运行。那么为什么没有抛出跨线程异常呢?还有更好的方法吗?
规则是您只能从创建 UI 元素的线程中与该元素进行交互。 UI 组件是线程仿射的。它们不仅不是线程安全的,甚至不是自由线程的。因此,在
Task.Run
委托中所做的事情要非常小心。使用 Progress<T>
是可以的,因为该组件在创建时捕获同步上下文,并通过捕获的同步上下文传播所有报告,从而有效地执行上述线程亲和性规则。
async void
和 await
Task.Run
。在 await
之后,您将返回 UI 线程。
private async void buttonRun_Click(object sender, EventArgs e)
{
CancellableProgressForm progressForm = new();
IProgress<int> progress = new Progress<int>(percent =>
{
progressForm.PercentComplete = percent;
});
await Task.Run(() => DoWork(progress));
progressForm.DialogResult = DialogResult.OK;
progressForm.Close();
DialogResult dr = progressForm.ShowDialog();
MessageBox.Show($"Result={dr}");
}
Progress<T>
类异步报告进度,这是高效的,因为它不会减慢后台工作,但它可能会产生一些奇怪的排序工件。如果您遇到问题,您可以尝试我在here发布的同步
IProgress<T>
实现。