【C#】async和await 续

前言

在文章《async和await》中,我们观察到了一下客观的规律,但是没有讲到本质,而且还遗留了一个问题:

这篇文章中,我们继续看看这个问题如何解决!

问题遗留

我们再看看之前写的代码:

static public void TestWait2()
{
     var t = Task.Factory.StartNew(async () =>
     {
         Console.WriteLine("Start");
         await Task.Delay(3000);
         Console.WriteLine("Done");
     });
     t.Wait();
     Console.WriteLine("All done");
 }

static public void TestWait3()
{
     var t = Task.Run(async () =>
     {
         Console.WriteLine("Start");
         await Task.Delay(3000);
         Console.WriteLine("Done");
     });
     t.Wait();
     Console.WriteLine("All done");
}

当时问题是,为啥 Task.Factory.StartNew 可以看到异步效果,而Task.Run中却是同步效果。
那其实是因为,Task.Factory.StartNew 返回的 t.Wait(); 它没卡住主线程,而Task.Run的 t.Wait();它卡住了。

那为啥,Task.Factory.StartNew没卡住呢?
这是应为 Task.Factory.StartNew 返回的变量 t 他是Task< Task >类型!

如果,Task.Run 返回的是Task类型,如果我们改成Task.Factory.StartNew,那么它 返回的类型就是Task>

在.Net4.0中提供一个Unwrap方法,用于将Task>解为Task< int>类型,所以如果代码改为:

static public async void Factory嵌套死等()
{
    Console.WriteLine($"Factory不嵌套死等{getID()}");
    var t = Task.Factory.StartNew(async() =>
    {
        Console.WriteLine($"Start{getID()}");
        await Task.Delay (1000);
        Console.WriteLine($"Done{getID()}");
    }).Unwrap();
    t.Wait();
    Console.WriteLine($"All done{getID()}");
}

那么此时 t.Wait(); 也能卡死主线程。

其实Task.Run(.net4.5引入) 是在 Task.Factory.StartNew(.net4.0引入) 之后出现的,Task.Run是为了简化Task.Factory.StartNew的使用。

t.Wait() 和 await t;

现在我从另一个角度分析问题。
使用 Task.Run,能不能达到异步的效果? 答案是肯定的!
不过,我们此时不应该使用 t.Wait(); 而是应该是 await t;

static public async void Run嵌套Await()
{
    Console.WriteLine($"Run嵌套Await{getID()}");
    var t = Task.Run(async () =>
    {
        Console.WriteLine($"Start{getID()}");
        await Task.Delay(1000);
        Console.WriteLine($"Done{getID()}");
    });
    await t;
    Console.WriteLine($"All done{getID()}");
}

【C#】async和await 续_第1张图片

这样的话就实现了异步效果。

await 是如何实现异步的

这里我们可以进一步分析一下。
“1” 是主线程的ID “5” 是 task 启的子线程 ID。
我发现All done 在 Done 后面执行的,这是应为 await t; 把主线程"遣返了"
而await t; 之后的代码(也就是All done 这句话的打印)是由子线程5接着完成的。

整个流程是这样的,当编译时,编译器看到了函数使用了 async 关键字,那么整个函数将被转换为一个带有状态机的函数,反编译后发现函数名称变为MoveNext。

当主线程执行到子函数时,遇到 await 那么此时 主线程就会返回(跳出整个子函数,去执行下一个函数),MoveNext呢就会切换状态机(由于状态机已经切换,下次MoveNext在被调用时,就会从await 处向下执行)。
不过,从现象看await 之后的代码,不是主线程调用了,而是Task的子线程。子线程会再次调用MoveNext,并且进入一个新的状态机。
这里就有一个结论,当主线程进入一个子函数,遇到await机会从函数直接返回,函数中以下的代码交给新的子线程执行。
为了证明一这一点,我又写了一个程序:

static public async Task AsyncInvoke()
{
    await Task.Run(() =>
    {
        Console.WriteLine($"This is 1 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    });
    Console.WriteLine($"1{getID()}");
    await Task.Run(() =>
    {
        Console.WriteLine($"This is 2 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    });
    Console.WriteLine($"2{getID()}");
    await Task.Run(() =>
    {
        Console.WriteLine($"This is 3 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    });
    Console.WriteLine($"3{getID()}");
    await Task.Run(() =>
    {
        Console.WriteLine($"This is 4 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    });
    Console.WriteLine($"4{getID()}");
}

执行效果如下:
【C#】async和await 续_第2张图片
你会发现不过一个函数里面有多少个await ,主线程遇到一个await就返回了,就跳出这个函数去执行其他的函数了。
函数剩下的await 后面的都是由子线程完成的!多个await 只是多个几个状态机而已。
所以在一个函数中,如果有个多个await ,除了第一个后面的都和主线程无关。

这里又出现了一个新的问题,为啥后面的线程ID都是5?这个其实不一定的,我重新跑了一次:
【C#】async和await 续_第3张图片
这次发现出现了两个子线程号 3 和 5,这是应为 Task 背后有个 线程池。Task 被翻译为任务,单纯的线程是指的Thread
Task 启动后,使用哪个线程是由背后的线程池提供,而这个线程池是由.net进行维护。包括回调什么时候发生都是由线程池中的一个线程通知Task对象!await 操作符 其实是 调用 Task对象的 ContinueWith,所以上面这段代码也可以这么写:

/// 
/// 回调式写法
/// 
public void TaskInvokeContinue()
{
    Task.Run(() =>
    {
        Console.WriteLine($"This is 1 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    }).ContinueWith(t =>
    {
        Console.WriteLine($"This is 2 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    }).ContinueWith(t =>
    {
        Console.WriteLine($"This is 3 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    }).ContinueWith(t =>
    {
        Console.WriteLine($"This is 4 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    })
    ;
    //不太爽---nodejs---回调式写法,嵌套是要疯掉
}

这就进一步体现了 await 用同步的方式,写异步的代码。
能实现这个的原因就是,函数已经被改造成一个状态机了。

到这里,我就把上次坑给填上了!下次我们在一起掰扯掰扯Task的一些有意思的用法。

小结

我觉得最重要的一点就是:
主线程遇到一个await就返回了,如何理解这个返回?
返回就是跳出这个函数,和这个函数没有半毛钱关系了,去执行其他下面的函数了。
该函数剩下的await后面的部分 都是由线程池中的子线程完成的!
理解这一点,有助于我们对异步代码的编写!

你可能感兴趣的:(C#基础语法,c#,开发语言)