Task.Run 其實就是啟動一個新的執行緒,但這個方法有更進階的功能,蠻方便的。
XAML
為了測試 Task.Run,所以簡化 UI 的設計如下
<Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfApp1" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="50"/> <RowDefinition/> </Grid.RowDefinitions> <Label Name="lbl" Grid.Row="0"/> <Button Grid.Row="1" x:Name="btn" Click="btn_Click" Content="開始"/> </Grid> </Window>
一般的執行緒
由底下的代碼可以看出,啟動新執行緒後,立即執行主執行緒的 MessageBox,過了 2 秒後才彈出新執行的MessageBox。
private void btn_Click(object sender, RoutedEventArgs e) { Task t = Task.Run( () => { Thread.Sleep(2000); MessageBox.Show("Sleep", "新執行緒"); } ); MessageBox.Show("Hello", "主執行緒"); }
await
在 Task.Run() 之前加上 await 運算子後,就會等待 Task.Run()裏面的新執行緒執行完畢,才會再度執行主執行緒。神奇的是,視窗並不會因此停頓。
另外請注意,一但加上 await(等待,非同步),btn_Click 方法就要加上 async(同步),宣告此方法要處於同步不停頓的狀況。await 及 async 需同時存在。
private async void btn_Click(object sender, RoutedEventArgs e) { await Task.Run( () => { Thread.Sleep(5000); MessageBox.Show(Thread.CurrentThread.Name,"test"); } ); MessageBox.Show("主執行緒","test"); }
返回值
如果新執行緒裏需要傳回某個值時,使用 return 即可,但 Task.Run 必需要有一個接收變數 result,如下所示。
private async void btn_Click(object sender, RoutedEventArgs e) { Task<string> task = Task.Run( () => { MessageBox.Show("Sleep", "新執行緒"); Thread.Sleep(1000); return "Hello"; } ); string result = await task; MessageBox.Show(result, "主執行緒"); }
也可以寫成如下
private async void btn_Click(object sender, RoutedEventArgs e)
{
string result = await Task.Run(
() => {
MessageBox.Show("Sleep", "新執行緒");
Thread.Sleep(1000);
return "Hello";
}
);
MessageBox.Show(result, "主執行緒");
}
方法
將新執行緒置於方法中,則方法必需傳回 Task<string> 物件。而方法中需 return Task.Run(),Run 方法裏的 Action 物件亦需 return “Hello”。
最後在主程式中使用 await Sleep()。
private async void btn_Click(object sender, RoutedEventArgs e) { string s = await Sleep(); MessageBox.Show(s,"主執行緒"); } private Task<string> Sleep() { return Task.Run( () => { MessageBox.Show("Sleep", "新執行緒"); Thread.Sleep(1000); return "Hello"; } ); }
新執行緒控制 UI 元件
在新執行緒裏是無法控制 UI 元件的,所以如果要在迴圈中改變元件,需使用 Dispatcher,如下
private async void btn_Click(object sender, RoutedEventArgs e)
{
await Task.Run(
() => {
for (var i = 0; i < 1000; i++)
{
Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => {
lbl1.Content = i.ToString();
}));
Thread.Sleep(1);
}
}
);
MessageBox.Show(index.ToString(), "主執行緒");
}
Dispatcher 陷井
Dispatcher 是等 UI 主執行緒有空時才會被執行的,如下可以驗証
private async void btn_Click(object sender, RoutedEventArgs e) { var index = 0; await Task.Run( () => { for (var i = 0; i < 1000; i++) { Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => { index++; lbl1.Content = i.ToString(); })); //Thread.Sleep(1); } } ); MessageBox.Show(index.ToString(), "主執行緒"); }
上述的迴圈跑了1000次,但 Dispatcher 其實才跑了 13~15次,實際次數依 CPU 速度而不同。
保証每次都會執行 Dispatcher
若要落實每個迴圈都跑一次 Dispatcher,則需在 Dispatcher 前加入 await,然後於 lambda 語法之前加入 async。底下的程式可以驗証 Dispatcher 確實跑了 1000 次。
private async void btn_Click(object sender, RoutedEventArgs e) { var index = 0; await Task.Run( async () => { for (var i = 0; i < 1000; i++) { await Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => { index++; lbl1.Content = i.ToString(); })); //Thread.Sleep(1); } } ); MessageBox.Show(index.ToString(), "主執行緒"); }