C# 使用 task 和 yield 通知 UI 一个 运行 进程
C# Using task and yield to keep UI informed of a running process
这样写代码是不是不好的做法。我想要完成的是用户可以按下控件上的按钮。该按钮启动某种分析过程,并且对于完成的每个项目,它都会向用户显示结果。
private IEnumerable<int> AnalyzeItems() {
for(int i = 0; i < 1000; i++) {
Thread.Sleep(500);
yield return i;
}
}
private void PerformTask_Click(object sender, EventArgs e) {
Task.Run(() => {
foreach (var item in AnalyzeItems()) {
ResultLog.Invoke((Action)delegate() { ResultLog.Text += item.ToString(); });
}
});
}
为什么不使用Backgroundworker?
首先将 backgroundworker 属性设置为:
- WorkerReportsProgress = true
- WorkerSupportsCancellation = true
这是代码:
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
for (int i = 0; i < 1000; i++) {
Thread.Sleep(500);
if (backgroundWorker1.CancellationPending) {
e.Cancel = true;
break;
}
backgroundWorker1.ReportProgress(i / 10, "step " + i);
}
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) {
label1.Text = e.UserState.ToString();
progressBar1.Value = e.ProgressPercentage;
}
private void button1_Click(object sender, EventArgs e) {
cancelButton.Focus();
button1.Enabled = false;
backgroundWorker1.RunWorkerAsync();
}
private void cancelButton_Click(object sender, EventArgs e) {
backgroundWorker1.CancelAsync();
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
button1.Enabled = true;
if (e.Error != null) {
MessageBox.Show(e.Error.Message, "Unexpected error");
}
if (e.Cancelled) {
MessageBox.Show("Process stopped by the user", "Cancelled");
}
label1.Text = "Press start";
progressBar1.Value = progressBar1.Minimum;
}
}
你的方法是不好的做法吗?这取决于。
如果您不希望 Task.Run
中的代码抛出任何异常并且您想继续做其他事情,那么您的代码是可以的。但是,如果您想捕获任何可能的异常并等待进程完成而不冻结 UI,那么您可能需要考虑使用 async/await
.
private async void PerformTask_Click(object sender, EventArgs e) {
try
{
await Task.Run(() => {
foreach (var item in AnalyzeItems()) {
ResultLog.Invoke((Action)delegate() { ResultLog.Text += item.ToString(); });
}
});
}
catch(Exception ex)
{
// handle...
}
}
替代方法是使用 IProgress<T>
。这允许轻松分离长时间 运行 工作和更新 UI。请注意,您不应过于频繁调用此方法,因为
- 这会给 UI 线程带来太多工作,导致 UI 冻结。
- 如果您将任何值类型传递给
IProgress<T>.Report
方法,它就会被复制。如果您过于频繁地调用它,您将冒 运行 垃圾收集器的风险,经常导致更大的冻结。
所有这些意味着您应该只将 IProgress
用于真正长时间的 运行 工作。
既然我们已经解决了所有问题,下面是一个示例,说明如何将已分析项目的进度通知用户:
private double _currentProgress;
public double CurrentProgress {
get => _currentProgress;
set
{
_currentProgress = value;
NotifyPropertyChanged();
}
}
private async void PerformTask_Click(object sender, EventArgs e)
{
var progress = new Progress<double>();
progress.ProgressChanged += (sender, p) => CurrentProgress = p;
await Task.Run(() => AnalyzeItems(Enumerable.Range(0, 5000).ToList(), progress));
}
private void AnalyzeItems(List<int> items, IProgress<double> progress)
{
for (int itemIndex = 0; itemIndex < items.Count; itemIndex++)
{
// Very long running CPU work.
// ...
progress.Report((double)itemIndex * 100 / items.Count);
}
}
如果 AnalyzeItems
单个项目花费的时间少于 100 毫秒,那么您不想在每个完成的项目之后报告(参见上面的原因)。您可以像这样决定更新状态的频率:
private void AnalyzeItems(List<int> items, IProgress<double> progress)
{
var lastReport = DateTime.UtcNow;
for (int itemIndex = 0; itemIndex < items.Count; itemIndex++)
{
// Very long running work.
Thread.Sleep(10);
// Tell the user what the current status is every 500 milliseconds.
if (DateTime.UtcNow - lastReport > TimeSpan.FromMilliseconds(500))
{
progress.Report((double)itemIndex * 100 / items.Count);
lastReport = DateTime.UtcNow;
}
}
}
如果您确实有很多非常快的迭代,您可能需要考虑将 DateTime.Now
更改为其他内容。
这样写代码是不是不好的做法。我想要完成的是用户可以按下控件上的按钮。该按钮启动某种分析过程,并且对于完成的每个项目,它都会向用户显示结果。
private IEnumerable<int> AnalyzeItems() {
for(int i = 0; i < 1000; i++) {
Thread.Sleep(500);
yield return i;
}
}
private void PerformTask_Click(object sender, EventArgs e) {
Task.Run(() => {
foreach (var item in AnalyzeItems()) {
ResultLog.Invoke((Action)delegate() { ResultLog.Text += item.ToString(); });
}
});
}
为什么不使用Backgroundworker?
首先将 backgroundworker 属性设置为:
- WorkerReportsProgress = true
- WorkerSupportsCancellation = true
这是代码:
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
for (int i = 0; i < 1000; i++) {
Thread.Sleep(500);
if (backgroundWorker1.CancellationPending) {
e.Cancel = true;
break;
}
backgroundWorker1.ReportProgress(i / 10, "step " + i);
}
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) {
label1.Text = e.UserState.ToString();
progressBar1.Value = e.ProgressPercentage;
}
private void button1_Click(object sender, EventArgs e) {
cancelButton.Focus();
button1.Enabled = false;
backgroundWorker1.RunWorkerAsync();
}
private void cancelButton_Click(object sender, EventArgs e) {
backgroundWorker1.CancelAsync();
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
button1.Enabled = true;
if (e.Error != null) {
MessageBox.Show(e.Error.Message, "Unexpected error");
}
if (e.Cancelled) {
MessageBox.Show("Process stopped by the user", "Cancelled");
}
label1.Text = "Press start";
progressBar1.Value = progressBar1.Minimum;
}
}
你的方法是不好的做法吗?这取决于。
如果您不希望 Task.Run
中的代码抛出任何异常并且您想继续做其他事情,那么您的代码是可以的。但是,如果您想捕获任何可能的异常并等待进程完成而不冻结 UI,那么您可能需要考虑使用 async/await
.
private async void PerformTask_Click(object sender, EventArgs e) {
try
{
await Task.Run(() => {
foreach (var item in AnalyzeItems()) {
ResultLog.Invoke((Action)delegate() { ResultLog.Text += item.ToString(); });
}
});
}
catch(Exception ex)
{
// handle...
}
}
替代方法是使用 IProgress<T>
。这允许轻松分离长时间 运行 工作和更新 UI。请注意,您不应过于频繁调用此方法,因为
- 这会给 UI 线程带来太多工作,导致 UI 冻结。
- 如果您将任何值类型传递给
IProgress<T>.Report
方法,它就会被复制。如果您过于频繁地调用它,您将冒 运行 垃圾收集器的风险,经常导致更大的冻结。
所有这些意味着您应该只将 IProgress
用于真正长时间的 运行 工作。
既然我们已经解决了所有问题,下面是一个示例,说明如何将已分析项目的进度通知用户:
private double _currentProgress;
public double CurrentProgress {
get => _currentProgress;
set
{
_currentProgress = value;
NotifyPropertyChanged();
}
}
private async void PerformTask_Click(object sender, EventArgs e)
{
var progress = new Progress<double>();
progress.ProgressChanged += (sender, p) => CurrentProgress = p;
await Task.Run(() => AnalyzeItems(Enumerable.Range(0, 5000).ToList(), progress));
}
private void AnalyzeItems(List<int> items, IProgress<double> progress)
{
for (int itemIndex = 0; itemIndex < items.Count; itemIndex++)
{
// Very long running CPU work.
// ...
progress.Report((double)itemIndex * 100 / items.Count);
}
}
如果 AnalyzeItems
单个项目花费的时间少于 100 毫秒,那么您不想在每个完成的项目之后报告(参见上面的原因)。您可以像这样决定更新状态的频率:
private void AnalyzeItems(List<int> items, IProgress<double> progress)
{
var lastReport = DateTime.UtcNow;
for (int itemIndex = 0; itemIndex < items.Count; itemIndex++)
{
// Very long running work.
Thread.Sleep(10);
// Tell the user what the current status is every 500 milliseconds.
if (DateTime.UtcNow - lastReport > TimeSpan.FromMilliseconds(500))
{
progress.Report((double)itemIndex * 100 / items.Count);
lastReport = DateTime.UtcNow;
}
}
}
如果您确实有很多非常快的迭代,您可能需要考虑将 DateTime.Now
更改为其他内容。