C#的asyn和await,测试、应用、原理
static void Main(string[] args) { var d = new NavDownLoader(); Task<bool> success = d.DownLoadLatestNavAsync(); Console.WriteLine("等...."); Console.WriteLine("结果:" + success.Result.ToString());
Console.WriteLine("完成");
Console.ReadKey();
}
已知DownLoadLatestNavAsync()是一个耗时10s+的方法,现在这个代码,效果就是:
1. 马上打印 "等...."
2. 在10s + 之后,打印 “结果:false”;
3. 在第2步之后,马上打印 “完成”
public class NavDownLoader { /// <summary> /// 异步下载 /// </summary> /// <returns></returns> public async Task<bool> DownLoadLatestNavAsync() { bool success = false; await Task.Run(new Action(() => { success = DownLoadLatestNav(); })); return success; } /// <summary> /// 同步下载 /// </summary> /// <returns></returns> public bool DownLoadLatestNav() { Thread.Sleep(10000); } }
在DownLoadLatestNavAsync(),中:
1. 等了10s才执行到:return success; 也就是说,await这句,对于DownLoadLatestNavAsync()是阻塞的。
而且,async 方法中,如果不添加await 关键字,会有提示警告!告诉你这个方法将会同步执行。(我理解是,被它的调用者(main函数)同步执行,不执行完它,它的调用者就进行不下去)
结论:
1. 调用async方法时,是异步调用的,马上去执行下面的语句。
2. 如果执行完主函数的语句后,使用Task.Result获取结果的时候,如果async方法没完成,会卡在那里。
3. 在async方法体中,要有 “await + 耗时操作” 这样的语句,而且这个语句会阻塞它后面的语句。
4. await + 耗时操作语句,也可以表现为 bool success = await DownLoadLatestNav(); 要注意的是,此时 public async Task<bool> DownLoadLatestNav() ,忽略掉await的警告即可。
*在习惯上应该await xxxx_async();await关键字,是说明,最终在哪个地方“耗时”;
其他参考:https://www.cnblogs.com/liqingwen/p/5831951.html
Task类型的方法(返回Task<T>的方法),在被调用时,会在方法的前面加上await,表示需要等待此方法的执行结果,再继续执行后面的代码。但如果不加await时,则不会等待方法的执行结果,进而也不会阻塞主线程。
根据《深入理解C#》
class AsyncForm : Form { Label label; Button button; public AsyncForm() { label = new Label { Location = new Point( 10, 20), Text = "Length" }; button = new Button { Location = new Point( 10, 50), Text = "Click" }; button. Click += DisplayWebSiteLength; //❶ 包装 事件 处理 程序 Controls. Add( label); Controls. Add( button); } async void DisplayWebSiteLength( object sender, EventArgs e) { label. Text = "Fetching..."; using (HttpClient client = new HttpClient()) { string text = await client. GetStringAsync(" http:// csharpindepth. com"); /*❷ 开始 获取 页面 */ label. Text = text. Length. ToString(); //❸ 更新 UI } }
}
其中,DisplayWebSiteLength方法又可以写为
async void DisplayWebSiteLength( object sender, EventArgs e) { label. Text = "Fetching..."; using (HttpClient client = new HttpClient()) { Task< string> task = client. GetStringAsync(" http:// csharpindepth. com"); string text = await task; label. Text = text. Length. ToString(); } }
private void Btn_test_Click(object sender, RoutedEventArgs e) { BackgroundWorker bc = new BackgroundWorker(); bc.DoWork += (ss, ee) => { Thread.Sleep(10000); }; bc.RunWorkerCompleted += (ss, ee) => { btn_test.Content = "完成测试"; }; bc.RunWorkerAsync(); }
可以说明几点:
1. task的类型是Task<string>,而await task的类型是string。也就是说,await起到了类似于"拆包"的作用。
2. 方法在执行await表达式时就返回了,在这个语句之后的语句,又是在UI线程上执行的。(这点至关重要)类似于BackgroundWorker的RunWorkerCompleted的事件回调的方法体,也是在UI线程上执行。
3. await的作用是避免线程阻塞,但是执行完await 表达式后,又能回到“主”线程上执行后续的代码。
其实Task类包含了Task.ContinueWith的方法,那么是不是await就是一个 “ 语法糖 ” ,使用了Task.ContinueWith??
4. async就是告诉调用者,执行这个方法就不要在意它什么时候完成的,async本身写不写都无所谓。因此,Click事件可以使用async方法订阅。
async告诉调用者,可能返回void、Task、Task<TResult>,其中void是专门给事件订阅用的(书上说的),其他情况应该返回Task。
public async Task< int> FooAsync() { string bar = await BarAsync(); // 显然 通常 会 更加 复杂 return bar. Length; } public async Task< string> BarAsync() { // 一些 异步 代码, 可能 会 调用 更多 的 异步 方法 }
书上的话:
“ 你 可以 像 阅读 同步 代码 那样 去 阅读 异步 代码, 只需 留意 代码 异步 等待 某些 操作 完成时 的 位置 即可。 然后, 当 遇到 代码 不按 预期 执行 这种 棘手 问题 时, 可 深入研究 一下 哪些 地方 涉及 到了 哪些 线程, 以及 调用 栈 在任 意 时间 点 的 样子。"
“
在遇到一个真正异步await表达式之前,方法的执行是完全同步的。调用异步方法,与在单独的线程池中启动的方法不同,应确保总是编写快速返回的异步方法,避免在异步方法中执行耗时方法,否则为其创建一个Task。
”
重点解读:
async System.Threading.Tasks.Task Test() { Thread.Sleep(10000); await System.Threading.Tasks.Task.Run(() => { Thread.Sleep(20000); }); btn_test.Content = "完成Task"; Thread.Sleep(10000); } private async void Btn_test_Click(object sender, RoutedEventArgs e) { await Test(); btn_test.Content = "完成测试"; return; }
上面的结果是:
1 . 先UI卡顿10s。
2. UI解除卡顿,然后等待了20s,期间UI不卡顿
3. 执行btn_test.content = "完成Task";
4. UI卡顿10s
5. 解除卡顿, 然后执行btn_test.content = "完成测试"
猜测:“ 主 ” 线程是要一直遇到 “ 最底层 ” 的await,才会 “返回 ”。
那么,值得注意的地方是,不要:
注意:如果
async System.Threading.Tasks.Task Test() { System.Threading.Tasks.Task.Run(() => { Thread.Sleep(20000); }).Wait(); }
含义将完全不同,这段代码是在UI线程上执行的,将会直接卡顿20s
task.Wait()是在主线程上执行的。
注意,在Winform或WPF的UI线程中,下列代码会发生【死锁】。在控制台或新的线程反而不会。
async System.Threading.Tasks.Task Test() { btn_test.Content = "执行1"; await System.Threading.Tasks.Task.Run(() => { Thread.Sleep(20000); MessageBox.Show("finish"); }); btn_test.Content = "执行2"; } private async void Btn_test_Click(object sender, RoutedEventArgs e) { var task = Test(); btn_test.Content = "执行0"; task.Wait(); return; }
执行步骤:
1. 进入test(),执行btn_test.Content="执行1",await语句。
2. 返回,并执行btn_test.Conent="执行0";
3. 一直卡顿,20s后弹出Message,但是不能执行btn_test.Conent="执行2";
4. 一直卡顿。。。
猜测:
1. task.Wait()会锁定UI线程。
1. await内的语句执行完毕后,后续btn_test.Conent="执行2"想要申请UI线程,但是此时被 “ task.Wait() ” 锁定导致不能 “回调 ”
解决办法:
将task.wait()注释掉,用回await test();
顺序会变为:btn_test.Content="执行1",btn_test.Content="执行2",btn_test.Content="执行0"
异常处理
static async Task MainAsync() { Task< string> task = ReadFileAsync(" garbage file"); //❶ 开始 异步 读取 try { string text = await task; //❷ 等待 内容 Console. WriteLine(" File contents: {0}", text); } catch (IOException e) //❸ 处理 IO 失败 { Console. WriteLine(" Caught IOException: {0}", e. Message); } }
异常处理也是表现得像同步代码一样。
static async Task MainAsync() { Task< int> task = ComputeLengthAsync( null); //故意 传入 错误 的 参数 Console. WriteLine(" Fetched the task"); int length = await task; //❶ 等待 结果 Console. WriteLine(" Length: {0}", length); } static async Task< int> ComputeLengthAsync( string text) { if (text == null) { throw new ArgumentNullException(" text"); // ❷ 立即 抛出 异常 } await Task. Delay( 500); //模拟 真实 的 异步 工作 return text. Length; }
效果:
1. 打印 Fetched the task
2. 执行到1的await时候,才会发生异常
async System.Threading.Tasks.Task Test() { await System.Threading.Tasks.Task.Run(() => { throw new IOException(); }); btn_test.Content = "执行1";//【后续操作】 } private async void Btn_test_Click(object sender, RoutedEventArgs e) { try { await Test(); } catch (IOException ex) { } btn_test.Content = "执行0";//【后续操作】 }
效果:
1. btn_test.content = "执行1"是不会执行的。
2. Btn_test_Click中,能捕获到IOException。
深入一些:
书上说:
Task或Task<TResult>其实是一个token,token表示在这个操作完成前,不能进行下一步处理。
疑问:什么是token,token是不是对于“主”线程有用的东西??
这是从主观上的感觉,不代表实际执行机制
骨架方法,是指,由编译器对原方法,进行“改进”的,方法名和 “ 主 ” 线程方法名相同的方法。
“ 【骨架方法】 需要 创建【状态机】, 并 执行 一个 步骤( 此处 的 步骤 指 执行 第一个 await 表达式 之前 的 代码), 然后 返回 一个 表示 状态 机 进度 的 任务。( 别忘了, 在 第一次 到达 真正 需要 等待 的 await 表达式 之前, 执行过程是同步的。) 此后, 骨架 方法 的 运作 就此 结束。”
“【状态机】会负责其余事项, 【后续操作】 附加 到 其他 异步 操作 后, 可通 知 【状态机】”
--【后续操作】就是await task之后的语句,感觉上就是返回主线程一样的操作。