在WPF中,在使用多線程在後臺進行計算限制的異步操做的時候,若是在後臺線程中對UI進行了修改,則會出現一個錯誤:(調用線程沒法訪問此對象,由於另外一個線程擁有該對象。)這是很常見的一個錯誤,一不當心就會有這個現象。在WPF中,若是不是用多線程的話,例如單線程應用程序,就是說代碼一路過去都在GUI線程運行,能夠隨意更新任何東西,包括UI對象。可是使用多線程來更新UI就可能會出現以上所說問題,怎麼解決?本文章提供兩個方法:Dispatcher(大部分人使用),TaskScheduler(任務調度器)。程序員
問題再現多線程
可能有的WPF新手不懂這是什麼狀況,先來個問題的再現,再使用本文章的兩個方法進行解決。dom
爲了演示方便,我使用了最簡單的佈局,一個開始按鈕,三個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; } }
運行一下,在點擊開始按鈕的時候,獲得了一個錯誤信息:this
果真不出所料,Begin函數是在後臺線程執行的,tb這個TextBlock是前臺UI線程的對象,因此沒法在後臺線程改變UI線程擁有的對象,不少有點經驗的WPF程序員就會使用下面我要說的Dispatcher了!spa
問題解決線程
方法一:Dispatchercode
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下載。