Winforms 中工作线程更新界面的几种方式
2024-11-02 17:25:08

UI 元素通常是在主线程(UI线程)上创建和操作的。如果在非UI线程上直接更新 UI,可能会导致多个线程同时访问和修改UI元素,从而引发线程安全问题,例如竞态条件、死锁等。
在 Winforms 中,如果在工作线程上直接更新 UI,会引发 InvalidOperationException 异常,该异常表示跨线程操作无效。

Control.CheckForIllegalCrossThreadCalls

通过将 Control.CheckForIllegalCrossThreadCalls 属性设置为false来禁用跨线程调用的检查。

1
2
CheckForIllegalCrossThreadCalls = false;
Task.Run(() => label1.Text = "hello");

禁用此检查可能会导致线程安全问题和意外的行为。因此,通常不建议在生产环境中禁用Control.CheckForIllegalCrossThreadCalls检查。除非你知道自己在干什么。

BackgroundWorker

BackgroundWorker 是用于执行后台操作的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
private BackgroundWorker worker;

public Form1()
{
InitializeComponent();

// 创建BackgroundWorker实例
worker = new BackgroundWorker();

// 设置事件处理程序
worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;

// 启用报告进度和支持取消操作
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;
}

private void buttonStart_Click(object sender, EventArgs e)
{
// 启动后台操作
worker.RunWorkerAsync();
}

private void buttonCancel_Click(object sender, EventArgs e)
{
// 取消后台操作
if (worker.IsBusy)
{
worker.CancelAsync();
}
}

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
// 后台操作代码
for (int i = 1; i <= 100; i++)
{
// 检查是否取消操作
if (worker.CancellationPending)
{
e.Cancel = true;
return;
}

// 模拟耗时操作
Thread.Sleep(100);

// 报告进度
worker.ReportProgress(i);
}

// 操作完成后,可以将结果存储到e.Result中
e.Result = "操作完成!";
}

private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// 更新UI以显示进度信息
progressBar.Value = e.ProgressPercentage;
labelProgress.Text = $"进度:{e.ProgressPercentage}%";
}

private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// 处理操作完成后的结果
if (e.Cancelled)
{
labelResult.Text = "操作被取消";
}
else if (e.Error != null)
{
labelResult.Text = $"错误:{e.Error.Message}";
}
else
{
labelResult.Text = $"结果:{e.Result}";
}
}

从提供的成员方法来看,似乎很适合带有进度条的场景,比如下载、解压等。
BackgroundWorker 是较早的异步编程模型,功能相对有限。它不能很好地处理复杂的异步操作,如任务组合和异常处理等。所以不推荐使用。

Control.Invoke

Control.Invoke 方法允许在UI线程上执行代码。可以在工作线程中调用 Control.Invoke 方法,将 UI 更新的代码包装在一个委托中,然后将其传递给 Control.Invoke 来更新UI。

1
2
3
4
5
6
7
8
9
Task.Run(() =>
{
label1.Invoke(() =>
{
Thread.Sleep(1000);
label1.Text = "hello";
});
Debug.WriteLine("Done");
});

这是一个同步方法,会阻塞调用线程直到委托执行完成。

Control.BeginInvoke

Control.BeginInvoke 是 Invoke 的异步版本,不会阻塞调用线程。

1
2
3
4
5
6
7
8
9
Task.Run(() =>
{
label1.BeginInvoke(() =>
{
Thread.Sleep(1000);
label1.Text = "hello";
});
Debug.WriteLine("Done");
});

如果需要等待委托返回可以使用 Control.EndInvoke 方法。

1
2
3
4
5
6
7
8
9
10
11
Task.Run(() =>
{
var ar = label1.BeginInvoke(() =>
{
Thread.Sleep(1000);
label1.Text = "hello";
return "result";
});
var result = EndInvoke(ar); // 阻塞当前线程,并得到返回值"result"
Debug.WriteLine("Done");
});

SynchronizationContext

SynchronizationContext 是一个抽象类,用于提供线程间同步和上下文传递的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建UI同步上下文
var uiSyncContext = SynchronizationContext.Current;

Task.Run(() =>
{
Console.WriteLine("Background thread id: " + Thread.CurrentThread.ManagedThreadId);

// 模拟耗时操作
Thread.Sleep(2000);

// 使用UI同步上下文将任务调度到UI线程上执行
uiSyncContext.Send(_ =>
{
Console.WriteLine("UI thread id: " + Thread.CurrentThread.ManagedThreadId);

// 在UI线程上更新UI元素
MessageBox.Show("Task completed!");
}, null);
});

注意,Send 是同步的,会阻塞调用线程。而 Post 是异步版本。

TaskScheduler

TaskScheduler 是一个抽象类,用于定义任务的调度行为。
TaskScheduler.FromCurrentSynchronizationContext 是一个静态方法,它返回与当前同步上下文关联的 TaskScheduler 对象。它可用于在异步操作中将任务调度到当前同步上下文中执行,以确保操作在正确的线程上执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 使用 TaskScheduler.FromCurrentSynchronizationContext 获取与当前同步上下文关联的 TaskScheduler 对象
TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();

// 在后台线程上执行异步操作
await Task.Run(() =>
{
Console.WriteLine("Background thread id: " + System.Threading.Thread.CurrentThread.ManagedThreadId);

// 模拟耗时操作
Task.Delay(2000).Wait();

// 使用 TaskScheduler.FromCurrentSynchronizationContext 将任务调度到UI线程上执行
Task.Factory.StartNew(() =>
{
Console.WriteLine("UI thread id: " + System.Threading.Thread.CurrentThread.ManagedThreadId);

// 在UI线程上更新UI元素
MessageBox.Show("Task completed!");
}, System.Threading.CancellationToken.None, TaskCreationOptions.None, uiScheduler);
});

总结

以上这些都是在工作线程更新UI的一些常见方式。以下是对每种方案的简要总结:

  • Control.CheckForIllegalCrossThreadCalls:这是一种简单的方式。但是,这种方式是不安全的,可能导致UI线程和工作线程之间的竞态条件和其他线程安全问题。因此不推荐使用它。

  • BackgroundWorker:一个已经废弃的组件。功能相对有限,不适用于复杂的异步操作。因此不推荐使用它。

  • Control.InvokeControl.BeginInvoke:这是使用Control类提供的方法在工作线程上执行操作并在UI线程上更新UI的传统方式。Invoke是同步的,会阻塞调用线程,而BeginInvoke是异步的,不会阻塞。

  • SynchronizationContext:用于在不同线程之间同步操作。它提供了SendPost方法来将操作调度到关联的线程上执行。优点是它提供了更灵活的线程同步和调度机制,并且可以用于管理UI线程之外的其他线程。

  • TaskScheduler:用于调度和管理任务的抽象类。通过使用TaskScheduler.FromCurrentSynchronizationContext方法可以将任务调度到 UI 线程上执行。侧重点是任务调度。


只是更新 UI 控件的话,推荐用Control.InvokeControl.BeginInvoke