C#深入理解异步编程async/await

async/await 本身是个语法糖

看我这篇文章: https://blog.csdn.net/weixin_46879188/article/details/120849575

C#的另一个语法糖:用yield实现IEnumerable接口也是采用这种技术。

使用async/await和.Result和.Wait()和.GetAwaiter().GetResult()的区别

用法:
await搭配 (返回Task/Task<T>)使用。

.Wait()和GetAwaiter().GetResult() 搭配(返回Task/Task<T>)使用。

.Result只能搭配 (返回Task<T>)使用。

功能:

await是阻塞当前方法继续调用,但不阻塞当前线程;

.Result和.Wait()和.GetAwaiter().GetResult() 会在阻塞当前线程同时仍然会启动另外一个后台线程去执行这个函数。

在非常特殊的情况下,如果非得阻塞当前线程去执行这个 Task,可以考虑使用 .GetAwaiter().GetResult()。两者效果基本一样,阻塞当前线程同时会启动另外一个后台线程。但是如果使用 .Result 或 .Wait() 发生错误时会抛出 AggregateExecption,如果用Exception去也是可以捕捉到异常的 。而使用 .GetAwaiter().GetResult()则是会抛出实际的 Excepiton ,Excepiton 中的堆栈也符合我们所期望的。

主要是区分两个概念:
1.阻塞当前方法调用;
2.阻塞当前线程;

Async

Async 方法有三种可能的返回类型: Task、Task<T> 和 void。 当从同步转换为异步代码时,任何返回类型 T 的方法都会成为返回 Task<T> 的 async 方法,任何返回 void 的方法都会成为返回 Task 的 async 方法。返回 void 的 async 方法具有特定用途: 用于支持异步事件处理程序,async, await 底层是状态机, 而如果返回值是void的话,调度方是不会有等待行为的,因为没有awaiter(可以看看下面demo的static async void Test()方法)

如下Demo:

  /// <summary>
    /// 应用程序的主入口点。
    /// </summary>
    [STAThread]
    static void Main()
    {
        Console.WriteLine("我是主线程,线程ID:" + Thread.CurrentThread.ManagedThreadId);
        //task用法一  
        Task task1 = new Task(() => MyAction());
        task1.Start();

        //task用法二  
        var strRes = Task.Run<string>(() => { return GetReturnStr(); });
        Console.WriteLine(strRes.Result);

        Console.WriteLine("---------------------------------");
        Console.WriteLine("①我是主线程,线程ID:{0}", Thread.CurrentThread.ManagedThreadId);
		var testResult = TestAsync();
        testResult.GetAwaiter().OnCompleted(() => {
            var id = testResult.Id;
            Console.WriteLine("My name is: " + id);
        });
    
        var te = testResult.Result;
        Test();
        Console.WriteLine("主线程执行完毕");          
        Console.ReadKey();
    }
    static async Task TestAsync()
    {
        Console.WriteLine("②调用GetReturnResult()之前,线程ID:{0}。当前时间:{1}", Thread.CurrentThread.ManagedThreadId,
            DateTime.Now.ToString("yyyy-MM-dd hh:MM:ss"));
        var name = GetReturnResult();
        Console.WriteLine("④调用GetReturnResult()之后,线程ID:{0}。当前时间:{1}", Thread.CurrentThread.ManagedThreadId,
            DateTime.Now.ToString("yyyy-MM-dd hh:MM:ss"));
        Console.WriteLine("⑥得到GetReturnResult()方法的结果一:{0}。当前时间:{1}", await name,
            DateTime.Now.ToString("yyyy-MM-dd hh:MM:ss"));
        Console.WriteLine("⑥得到GetReturnResult()方法的结果二:{0}。当前时间:{1}", name.GetAwaiter().GetResult(),
            DateTime.Now.ToString("yyyy-MM-dd hh:MM:ss"));
    }

    static async Task<string> GetReturnResult()
    {
        Console.WriteLine("③执行Task.Run之前, 线程ID:{0}", Thread.CurrentThread.ManagedThreadId);

        return await Task.Run(() =>
        {
            Thread.Sleep(20000);
            Console.WriteLine("⑤GetReturnResult()方法里面线程ID: {0}", Thread.CurrentThread.ManagedThreadId);
            return "我是返回值";
        });
    }

    static void MyAction()
    {
        Console.WriteLine("我是新进程,线程ID:() MyAction " + Thread.CurrentThread.ManagedThreadId);
    }

    static string GetReturnStr()
    {
        return "我是返回值 ,GetReturnStr 线程ID:" + Thread.CurrentThread.ManagedThreadId;
    }
    static async void Test()
    {
        Console.WriteLine("Test 线程ID:" + Thread.CurrentThread.ManagedThreadId);
        await Task.Run(() => {
            Thread.Sleep(5000);
            Console.WriteLine("Test方法等待5秒开始执行");
        });
        Console.WriteLine("Test 线程ID:" + Thread.CurrentThread.ManagedThreadId);
    }

输出结果如下:
在这里插入图片描述

Execution Context 执行上下文

它在多线程的好比空气:你可以不知道它,但它非常重要。ExecutionContext是为了解决线程本地存储在多线程中无法传递的问题:总得有一种机制能够传递全局信息。否则只能通过函数调用参数传递了。
当一个线程发起异步调用的时候,ExecutionContext会自动的在线程之间传递以下信息:
线程安全设置
Host设置(与web服务有关)
Logical Call Context, 可以在其中保存和传递对象。
线程的Culture(从.NET 4.6以后)。

Synchronization Context 同步上下文

它是为了描述异步调用返回时的行为所创建的抽象。它有两个基本接口方法:
Send 同步地等待任务执行完毕。
Post 把任务发出去就不管了。
那么异步调用返回时的行为是什么意思?既然是抽象,那就会有具体的实现。后面我们会看到几种实现。
当开始异步调用时,C#会捕获(capture)当前线程的同步上下文,并保存到Task中。在异步调用返回时,需要恢复(resume)同步上下文。此时就会调用同步上下文的Send或者Post。
下面是几种典型的同步上下文实现:
1.UI同步上下文。由于UI界面操作必须在UI线程中进行,因此这个上下文做的事情就是把需要恢复的工作Marshal起来交给UI线程。(可能有人会好奇如何交给UI线程去做。简单来说, UI线程有个Windows消息循环,同步上下文将任务封装在一个特定消息中,UI线程得到这个消息后,就去执行其中的任务)。
2.
ASP.NET同步上下文。它有以下特点:
3.
①不会切换线程,因为后台线程没什么区别。
②会把线程的Principle和Culture传递过去。(因为ASP.NET依赖于此)
③在异步页面中记录尚未完成的IO数量。
4.默认同步上下文。就是线程池的调度器,基本上没有特别的操作。
最后,调用ConfigureAwait(false)时,就会跳过恢复同步上下文这一过程。所以,有时候必要(当没必要传递任何信息时,使用它可以提高效率),有时候又会出错。例如,UI程序的异步调用本来没问题,你加了这个语句,反而会造成修改界面的操作可能不在UI线程中执行,从而出错。但是注意,无论如何,执行上下文都是会传递的。
结合以上,第一段程序的更精确的编译后版本是这样的:

private void btnDoStuff_Click(int step)
{
    switch (step)
    {
        case 0:
            lblStatus.Content = "Doing Stuff";
            Task t = Task.Delay(4000);
            t.ContinueWith(
                task => SynchronizationContext.Current.Post(
                 state => btnDoStuff_Click(task.Step), 
                 task)
            );
            break;
        case 1:
            lblStatus.Content = "After await";
            break;
    }
}

到底是谁在执行异步调用?
这个问题曾经困扰我很久。如果我当前的线程调用一个异步调用后返回了,那到底是谁在完成真正调用的工作呢?答案是一个(或几个)共享的线程:线程池中的IO线程。
如下是一段代码:

async void GetButton_OnClick(object o, EventArgs e)
{
    Task<Image> task = GetFaviconAsync(_url);

    Image image = await task;

    AddAFavicon(image);
}

async Task<Image> GetFaviconAsync(string url)
{
    var task = _webClient.DownloadDataTaskAsync(url);

    byte[] bytes = await task;

    return MakeImage(bytes);
}

线程的执行情况如下图:

在这里插入图片描述

大部分的时间都在用户线程中。只有调用到非常底层,IO完成之后,才有IO线程被唤醒(见11),然后它调用Task的同步上下文的Post,将剩下的任务再交给用户线程去执行(Winform是委托UI线程。ASP.NET是可能是发起请求的线程,但更有可能是操作完成时空闲的任何线程。)。
下面是一个动态的解释:
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值