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线程上执行的(这点至关重要)类似于BackgroundWorkerRunWorkerCompleted的事件回调的方法体,也是在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。

 


 

深入一些:

书上说:

TaskTask<TResult>其实是一个token,token表示在这个操作完成前,不能进行下一步处理。

疑问:什么是token,token是不是对于“主”线程有用的东西??

 

 

这是从主观上的感觉,不代表实际执行机制

 

 


骨架方法,是指,由编译器对原方法,进行“改进”的,方法名和 “ 主 ” 线程方法名相同的方法。

“ 【骨架方法】 需要 创建【状态机】, 并 执行 一个 步骤( 此处 的 步骤 指 执行 第一个 await 表达式 之前 的 代码), 然后 返回 一个 表示 状态 机 进度 的 任务。( 别忘了, 在 第一次 到达 真正 需要 等待 的 await 表达式 之前, 执行过程是同步的。) 此后, 骨架 方法 的 运作 就此 结束。

 

【状态机】会负责其余事项, 【后续操作】 附加 到 其他 异步 操作 后, 可通 知 【状态机】

--【后续操作】就是await task之后的语句,感觉上就是返回主线程一样的操作。

 

 

posted on 2019-02-19 15:55  耀礼士多德  阅读(661)  评论(0编辑  收藏  举报