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();
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 = "操作完成!"; }
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e) { 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); Debug.WriteLine("Done"); });
|
SynchronizationContext
SynchronizationContext 是一个抽象类,用于提供线程间同步和上下文传递的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| var uiSyncContext = SynchronizationContext.Current;
Task.Run(() => { Console.WriteLine("Background thread id: " + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);
uiSyncContext.Send(_ => { Console.WriteLine("UI thread id: " + Thread.CurrentThread.ManagedThreadId);
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 uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
await Task.Run(() => { Console.WriteLine("Background thread id: " + System.Threading.Thread.CurrentThread.ManagedThreadId);
Task.Delay(2000).Wait();
Task.Factory.StartNew(() => { Console.WriteLine("UI thread id: " + System.Threading.Thread.CurrentThread.ManagedThreadId);
MessageBox.Show("Task completed!"); }, System.Threading.CancellationToken.None, TaskCreationOptions.None, uiScheduler); });
|
总结
以上这些都是在工作线程更新UI的一些常见方式。以下是对每种方案的简要总结:
Control.CheckForIllegalCrossThreadCalls
:这是一种简单的方式。但是,这种方式是不安全的,可能导致UI线程和工作线程之间的竞态条件和其他线程安全问题。因此不推荐使用它。
BackgroundWorker
:一个已经废弃的组件。功能相对有限,不适用于复杂的异步操作。因此不推荐使用它。
Control.Invoke
和Control.BeginInvoke
:这是使用Control
类提供的方法在工作线程上执行操作并在UI线程上更新UI的传统方式。Invoke
是同步的,会阻塞调用线程,而BeginInvoke
是异步的,不会阻塞。
SynchronizationContext
:用于在不同线程之间同步操作。它提供了Send
和Post
方法来将操作调度到关联的线程上执行。优点是它提供了更灵活的线程同步和调度机制,并且可以用于管理UI线程之外的其他线程。
TaskScheduler
:用于调度和管理任务的抽象类。通过使用TaskScheduler.FromCurrentSynchronizationContext
方法可以将任务调度到 UI 线程上执行。侧重点是任务调度。
只是更新 UI 控件的话,推荐用Control.Invoke
和Control.BeginInvoke
。