前言
在WPF中,在使用多線程在后臺進行計算限制的異步操作的時候,如果在后臺線程中對UI進行了修改,則會出現一個錯誤:(調用線程無法訪問此對象,因為另一個線程擁有該對象。)這是很常見的一個錯誤,一不小心就會有這個現象。在WPF中,如果不是用多線程的話,例如單線程應用程序,就是說代碼一路過去都在GUI線程運行,可以隨意更新任何東西,包括UI對象。但是使用多線程來更新UI就可能會出現以上所說問題,怎么解決?本文章提供兩個方法:Dispatcher(大部分人使用),TaskScheduler(任務調度器)。
?
問題再現
可能有的WPF新手不懂這是什么情況,先來個問題的再現,再使用本文章的兩個方法進行解決。
為了演示方便,我使用了最簡單的布局,一個開始按鈕,三個TextBlock。按一下開始按鈕,開一個后臺線程隨機得到一個數字,并且更新第一個TextBlock。再開另外一個后臺線程得到另外一個數字,更新第二個TextBlock。第三個TextBlock處理同理。
XAML代碼:
<Window x:Class="UpdateUIDemo.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"Title="MainWindow" Height="130" Width="363"><Canvas><TextBlock Width="40" Canvas.Left="38" Canvas.Top="27" Height="29" x:Name="first" Background="Black" Foreground="White"></TextBlock><TextBlock Width="40" Canvas.Left="128" Canvas.Top="27" Height="29" x:Name="second" Background="Black" Foreground="White"></TextBlock><TextBlock Width="40" Canvas.Left="211" Canvas.Top="27" Height="29" x:Name="Three" Background="Black" Foreground="White"></TextBlock><Button Height="21" Width="50" Canvas.Left="271" Canvas.Top="58" Content="開始" Click="Button_Click"></Button></Canvas> </Window>
后臺代碼:
public partial class MainWindow : Window{public MainWindow(){InitializeComponent();}private void Button_Click(object sender, RoutedEventArgs e){Task.Factory.StartNew(Work);}private void Work(){Task task = new Task((tb) => Begin(this.first), this.first);Task task2 = new Task((tb) => Begin(this.second), this.first);Task task3 = new Task((tb) => Begin(this.Three), this.first);task.Start();task.Wait();task2.Start();task2.Wait();task3.Start();}private void Begin(TextBlock tb){int i=100000000;while (i>0){i--;}Random random = new Random();String Num = random.Next(0, 100).ToString();tb.Text = Num;}}
?運行一下,在點擊開始按鈕的時候,得到了一個錯誤信息:
果然不出所料,Begin函數是在后臺線程執行的,tb這個TextBlock是前臺UI線程的對象,所以無法在后臺線程改變UI線程擁有的對象,很多有點經驗的WPF程序員就會使用下面我要說的Dispatcher了!
?
問題解決
方法一:Dispatcher
1.把UI更新的代碼放到一個函數中:
private void UpdateTb(TextBlock tb, string text){tb.Text = text;}
2.使用Dispatcher,大家看修改后的Begin函數(紅色內容):
private void Begin(TextBlock tb){int i=100000000;while (i>0){i--;}Random random = new Random();String Num = random.Next(0, 100).ToString();Action<TextBlock, String> updateAction = new Action<TextBlock, string>(UpdateTb);tb.Dispatcher.BeginInvoke(updateAction,tb,Num);}
再運行一次程序,可以看到能正常顯示了,并且不會出現假死現象。
方法二:任務調度器(TaskScheduler)
有很多任務調度器,在CLR Var C#中就提出了線程池任務調度器,I/O任務調度器,任務限時調度器等,調度器的職責就是負責任務的調度,調節任務執行。同步上下文任務調度器就是該方法二所使用的調度器,其作用是將所有任務都調度給應用程序的GUI線程。
public partial class MainWindow : Window{public MainWindow(){InitializeComponent();}private readonly TaskScheduler _syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();private void Button_Click(object sender, RoutedEventArgs e){Task.Factory.StartNew(SchedulerWork);}private void SchedulerWork(){Task.Factory.StartNew(Begin, this.first).Wait();Task.Factory.StartNew(Begin, this.second).Wait();Task.Factory.StartNew(Begin, this.Three).Wait();}private void Begin(object obj){TextBlock tb = obj as TextBlock;int i = 100000000;while (i>0){i--;}Random random = new Random();String Num = random.Next(0,100).ToString();Task.Factory.StartNew(() => UpdateTb(tb, Num),new CancellationTokenSource().Token, TaskCreationOptions.None, _syncContextTaskScheduler).Wait();}private void UpdateTb(TextBlock tb, string text){tb.Text = text;}}
?
結果展示:
總結
任務調度器還有很多種,按照自己喜歡的方法來實現后臺多線程更新UI。還有任務調度器也可以應用到Winform中。下面提供示例Demo下載。