www.9778.com 20

C#语法——await与async的正确打开方式

C#5.0推出了新语法,await与async,但相信大家还是很少使用它们。关于await与async有很多文章讲解,但有没有这样一种感觉,你看完后,总感觉这东西很不错,但用的时候,总是想不起来,或者不知道该怎么用。

下文以个人对async/await的理解为基础进行一些说明。

  1. 调用流阻塞:不同于线程阻塞,调用流阻塞只对函数过程起作用,调用流阻塞表示在一次函数调用中,执行函数代码的过程中发生的无法继续往后执行,需要在函数体中的某个语句停止的情形;

C#中 Thread,Task,Async/Await,IAsyncResult 的那些事儿!,

说起异步,Thread,Task,async/await,IAsyncResult
这些东西肯定是绕不开的,今天就来依次聊聊他们

 

1.线程(Thread)

 

多线程的意义在于一个应用程序中,有多个执行部分可以同时执行;对于比较耗时的操作(例如io,数据库操作),或者等待响应(如WCF通信)的操作,可以单独开启后台线程来执行,这样主线程就不会阻塞,可以继续往下执行;等到后台线程执行完毕,再通知主线程,然后做出对应操作!

 

在C#中开启新线程比较简单

 

static void Main(string[] args)
{
    Console.WriteLine("主线程开始");
    //IsBackground=true,将其设置为后台线程
    Thread t = new Thread(Run) { IsBackground = true };
    t.Start();
   Console.WriteLine("主线程在做其他的事!");
    //主线程结束,后台线程会自动结束,不管有没有执行完成
    //Thread.Sleep(300);
    Thread.Sleep(1500);
    Console.WriteLine("主线程结束");
}
static void Run()
{
    Thread.Sleep(700);
    Console.WriteLine("这是后台线程调用");
}

 

执行结果如下图

 

www.9778.com 1

 

可以看到在启动后台线程之后,主线程继续往下执行了,并没有等到后台线程执行完之后。

 

为什么呢?我觉得大家的await与async的打开方式不正确。

  1. 调用流阻塞点:调用流阻塞中,执行流所停下来地方的那条语句;
  2. 调用流阻塞返回:不同于线程阻塞,调用流发生阻塞的时候,调用流会立即返回,在C#中,返回的对象可以是Task或者Task<T>
  3. 调用流阻塞异步完成跳转:当调用流阻塞点处的异步操作完成后,调用流被强制跳转回调用流阻塞点处执行下一个语句的情形;
  4. async传染:指的是根据C#的规定:若某个函数F的函数体中需要使用await关键字的函数必须以async标记,进一步导致需要使用await调用F的那个函数F’也必须以async标记的情况;
  5. Task对象的装箱与拆箱:指Task<T>和T能够相互转换的情况。
  6. 异步调用:指以await作为修饰前缀进行方法调用的调用形式,异步调用时会发生调用流阻塞。
  7. 同步调用:指不以await作为修饰前缀进行方法调用的调用形式,同步调用时不会发生调用流阻塞。

1.1 线程池

 

试想一下,如果有大量的任务需要处理,例如网站后台对于HTTP请求的处理,那是不是要对每一个请求创建一个后台线程呢?显然不合适,这会占用大量内存,而且频繁地创建的过程也会严重影响速度,那怎么办呢?

 

线程池就是为了解决这一问题,把创建的线程存起来,形成一个线程池(里面有多个线程),当要处理任务时,若线程池中有空闲线程(前一个任务执行完成后,线程不会被回收,会被设置为空闲状态),则直接调用线程池中的线程执行(例asp.net处理机制中的Application对象),使用事例:

 

for (int i = 0; i < 10; i++)
{
    ThreadPool.QueueUserWorkItem(m =>
    {
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString());
    });
}
Console.Read();

 

运行结果:

 

www.9778.com 2

 

可以看到,虽然执行了10次,但并没有创建10个线程。

 

1.2 信号量(Semaphore)

 

Semaphore负责协调线程,可以限制对某一资源访问的线程数量,这里对SemaphoreSlim类的用法做一个简单的事例:

 

static SemaphoreSlim semLim = new SemaphoreSlim(3); //3表示最多只能有三个线程同时访问
static void Main(string[] args)
{
    for (int i = 0; i < 10; i++)
    {
        new Thread(SemaphoreTest).Start();
    }
    Console.Read();
}
static void SemaphoreTest()
{
    semLim.Wait();
    Console.WriteLine("线程" + Thread.CurrentThread.ManagedThreadId.ToString() + "开始执行");
    Thread.Sleep(2000);
    Console.WriteLine("线程" + Thread.CurrentThread.ManagedThreadId.ToString() + "执行完毕");
    semLim.Release();
}

 

执行结果如下:

 

www.9778.com 3

 

www.9778.com 4

 

可以看到,刚开始只有三个线程在执行,当一个线程执行完毕并释放之后,才会有新的线程来执行方法!

 

除了SemaphoreSlim类,还可以使用Semaphore类,感觉更加灵活,感兴趣的话可以搜一下,这里就不做演示了!

 

 正确的打开方式

async/await用于异步操作。

2.Task

 

Task是.NET4.0加入的,跟线程池ThreadPool的功能类似,用Task开启新任务时,会从线程池中调用线程,而Thread每次实例化都会创建一个新的线程。

 

Console.WriteLine("主线程启动");
//Task.Run启动一个线程
//Task启动的是后台线程,要在主线程中等待后台线程执行完毕,可以调用Wait方法
//Task task = Task.Factory.StartNew(() => { Thread.Sleep(1500); Console.WriteLine("task启动"); });
Task task = Task.Run(() => { 
    Thread.Sleep(1500);
    Console.WriteLine("task启动");
});
Thread.Sleep(300);
task.Wait();
Console.WriteLine("主线程结束");

 

执行结果如下:

 

www.9778.com 5

 

开启新任务的方法:Task.Run()或者Task.Factory.StartNew(),开启的是后台线程要在主线程中等待后台线程执行完毕,可以使用Wait方法(会以同步的方式来执行)。不用Wait则会以异步的方式来执行。

 

比较一下Task和Thread:

 

static void Main(string[] args)
{
    for (int i = 0; i < 5; i++)
    {
        new Thread(Run1).Start();
    }
    for (int i = 0; i < 5; i++)
    {
        Task.Run(() => { Run2(); });
    }
}
static void Run1()
{
    Console.WriteLine("Thread Id =" + Thread.CurrentThread.ManagedThreadId);
}
static void Run2()
{
    Console.WriteLine("Task调用的Thread Id =" + Thread.CurrentThread.ManagedThreadId);
}

 

执行结果:

 

www.9778.com 6

 

可以看出来,直接用Thread会开启5个线程,用Task(用了线程池)开启了3个!

 

 

在使用C#编写GUI程序的时候,如果有比较耗时的操作(如图片处理、数据压缩等),我们一般新开一个线程把这些工作交给这个线程处理,而不放到主线程中进行操作,以免阻塞UI刷新,造成程序假死。

2.1 Task<TResult>

 

Task<TResult>就是有返回值的Task,TResult就是返回值类型。

 

Console.WriteLine("主线程开始");
//返回值类型为string
Task<string> task = Task<string>.Run(() => {
    Thread.Sleep(2000); 
    return Thread.CurrentThread.ManagedThreadId.ToString(); 
});
//会等到task执行完毕才会输出;
Console.WriteLine(task.Result);
Console.WriteLine("主线程结束");

 

运行结果:

 

www.9778.com 7

 

通过task.Result可以取到返回值,若取值的时候,后台线程还没执行完,则会等待其执行完毕!

 

简单提一下:

 

Task任务可以通过CancellationTokenSource类来取消,感觉用得不多,用法比较简单,感兴趣的话可以搜一下!

 

首先看下使用约束。

传统的做法是直接使用C#的Thread类(也存在别的方式,参考这篇文章)进行操作。传统的做法在复杂的应用编写中可能会出现回调地狱的问题,因此C#目前主要推荐使用async/await来进行异步操作。

3. async/await

 

async/await是C#5.0中推出的,先上用法:

 

static void Main(string[] args)
{
    Console.WriteLine("-------主线程启动-------");
    Task<int> task = GetStrLengthAsync();
    Console.WriteLine("主线程继续执行");
    Console.WriteLine("Task返回的值" + task.Result);
    Console.WriteLine("-------主线程结束-------");
}
static async Task<int> GetStrLengthAsync()
{
    Console.WriteLine("GetStrLengthAsync方法开始执行");
    //此处返回的<string>中的字符串类型,而不是Task<string>
    string str = await GetString();
    Console.WriteLine("GetStrLengthAsync方法执行结束");
    return str.Length;
}
static Task<string> GetString()
{
   //Console.WriteLine("GetString方法开始执行")
    return Task<string>.Run(() =>
    {
        Thread.Sleep(2000);
        return "GetString的返回值";
    });
}

 

async用来修饰方法,表明这个方法是异步的,声明的方法的返回类型必须为:void,Task或Task<TResult>。

 

await必须用来修饰Task或Task<TResult>,而且只能出现在已经用async关键字修饰的异步方法中。通常情况下,async/await成对出现才有意义,看看运行结果:

 

www.9778.com 8

 

可以看出来,main函数调用GetStrLengthAsync方法后,在await之前,都是同步执行的,直到遇到await关键字,main函数才返回继续执行。

 

那么是否是在遇到await关键字的时候程序自动开启了一个后台线程去执行GetString方法呢?

 

现在把GetString方法中的那行注释加上,运行的结果是:

 

www.9778.com 9

 

大家可以看到,在遇到await关键字后,没有继续执行GetStrLengthAsync方法后面的操作,也没有马上反回到main函数中,而是执行了GetString的第一行,以此可以判断await这里并没有开启新的线程去执行GetString方法,而是以同步的方式让GetString方法执行,等到执行到GetString方法中的Task<string>.Run()的时候才由Task开启了后台线程!

 

那么await的作用是什么呢?

 

可以从字面上理解,上面提到task.wait可以让主线程等待后台线程执行完毕,await和wait类似,同样是等待,等待Task<string>.Run()开始的后台线程执行完毕,不同的是await不会阻塞主线程,只会让GetStrLengthAsync方法暂停执行。

 

那么await是怎么做到的呢?有没有开启新线程去等待?

 

www.9778.com 10

 

只有两个线程(主线程和Task开启的线程)!至于怎么做到的(我也不知道……>_<),大家有兴趣的话研究下吧!

 

1、await 只能在标记了async的函数内使用。

async/await通过对方法进行修饰把C#中的方法分为同步方法和异步方法两类,异步方法命名约定以Async结尾。但是需要注意的是,在调用异步方法的时候,并非一定是以异步方式来进行调用,只有指定了以await为修饰前缀的方法调用才是异步调用

4.IAsyncResult

 

IAsyncResult自.NET1.1起就有了,包含可异步操作的方法的类需要实现它,Task类就实现了该接口

 

 

www.9778.com 11

 

在不借助于Task的情况下怎么实现异步呢?

 

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("主程序开始--------------------");
        int threadId;
        AsyncDemo ad = new AsyncDemo();
        AsyncMethodCaller caller = new AsyncMethodCaller(ad.TestMethod);
 
        IAsyncResult result = caller.BeginInvoke(3000,out threadId, null, null);
        Thread.Sleep(0);
        Console.WriteLine("主线程线程 {0} 正在运行.",Thread.CurrentThread.ManagedThreadId)
        //会阻塞线程,直到后台线程执行完毕之后,才会往下执行
        result.AsyncWaitHandle.WaitOne();
        Console.WriteLine("主程序在做一些事情!!!");
        //获取异步执行的结果
        string returnValue = caller.EndInvoke(out threadId, result);
        //释放资源
        result.AsyncWaitHandle.Close();
        Console.WriteLine("主程序结束--------------------");
        Console.Read();
    }
}
public class AsyncDemo
{
    //供后台线程执行的方法
    public string TestMethod(int callDuration, out int threadId)
    {
        Console.WriteLine("测试方法开始执行.");
        Thread.Sleep(callDuration);
        threadId = Thread.CurrentThread.ManagedThreadId;
        return String.Format("测试方法执行的时间 {0}.", callDuration.ToString());
    }
}
public delegate string AsyncMethodCaller(int callDuration, out int threadId);

 

关键步骤就是红色字体的部分,运行结果:

 

www.9778.com 12

 

和Task的用法差异不是很大!result.AsyncWaitHandle.WaitOne()就类似Task的Wait。

 

5.Parallel

 

最后说一下在循环中开启多线程的简单方法:

 

Stopwatch watch1 = new Stopwatch();
watch1.Start();
for (int i = 1; i <= 10; i++)
{
    Console.Write(i + ",");
    Thread.Sleep(1000);
}
watch1.Stop();
Console.WriteLine(watch1.Elapsed);
Stopwatch watch2 = new Stopwatch();
watch2.Start();
//会调用线程池中的线程
Parallel.For(1, 11, i =>
{
    Console.WriteLine(i + ",线程ID:" + Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(1000);
});
watch2.Stop();
Console.WriteLine(watch2.Elapsed);

 

运行结果:

 

www.9778.com 13

 

循环List<T>:

 

List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 6, 7, 8, 9 };
Parallel.ForEach<int>(list, n =>
{
    Console.WriteLine(n);
    Thread.Sleep(1000);
});

 

执行Action[]数组里面的方法:

 

Action[] actions = new Action[] { 
   new Action(()=>{
       Console.WriteLine("方法1");
   }),
    new Action(()=>{
       Console.WriteLine("方法2");
   })
};
Parallel.Invoke(actions);

2、await 等待的函数必须标记async。

考虑以下C#程序:

 

有没有感觉这是个循环?没错,这就是个循环。这也就是为什么大家不怎么用他们的原因。这个循环很讨厌,那么怎么破除这个循环呢?

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading;using System.Threading.Tasks;namespace ConsoleApp1{ class Program { static void Main(string[] args) { TestMain(); } static void TestMain() { Console.Out.Write("Startn"); GetValueAsync(); Console.Out.Write; Console.ReadKey(); } static async Task GetValueAsync() { await Task.Run=> { Thread.Sleep; for(int i = 0; i < 5; ++i) { Console.Out.WriteLine(String.Format("From task : {0}", i)); } }); Console.Out.WriteLine("Task End"); } }}

6.异步的回调

 

为了简洁(偷懒),文中所有Task<TResult>的返回值都是直接用task.result获取,这样如果后台任务没有执行完毕的话,主线程会等待其执行完毕。这样的话就和同步一样了,一般情况下不会这么用。简单演示一下Task回调函数的使用:

 

Console.WriteLine("主线程开始");
Task<string> task = Task<string>.Run(() => {
    Thread.Sleep(2000); 
    return Thread.CurrentThread.ManagedThreadId.ToString(); 
});
//会等到任务执行完之后执行
task.GetAwaiter().OnCompleted(() =>
{
    Console.WriteLine(task.Result);
});
Console.WriteLine("主线程结束");
Console.Read();

 

执行结果:

 

www.9778.com 14

 

OnCompleted中的代码会在任务执行完成之后执行!

 

另外task.ContinueWith()也是一个重要的方法:

 

Console.WriteLine("主线程开始");
Task<string> task = Task<string>.Run(() => {
    Thread.Sleep(2000); 
    return Thread.CurrentThread.ManagedThreadId.ToString(); 
});
task.GetAwaiter().OnCompleted(() =>
{
    Console.WriteLine(task.Result);
});
task.ContinueWith(m=>{Console.WriteLine("第一个任务结束啦!我是第二个任务");});
Console.WriteLine("主线程结束");
Console.Read();

 

执行结果:

 

www.9778.com 15

 

ContinueWith()方法可以让该后台线程继续执行新的任务。

 

Task的使用还是比较灵活的,大家可以研究下,好了,以上就是全部内容了,篇幅和能力都有限,希望对大家有用!

Thread,Task,Async/Await,IAsyncResult
的那些事儿!, 说起异步,Thread,Task,async/await,IAsyncResult
这些东西肯定是绕不开的,今天就来依次…

【很简单,await等待的是线程,不是函数。】

在我的计算机上,执行该程序得到以下结果:

不理解吗?没关系,接着看下去。

StartEndFrom task : 0

下面从头来讲解,首先看这么一组对比

From task : 1From task : 2From task : 3From task : 4Task End

public static int NoAsyncTest()
{
   return 1;
}
public static async Task<int> AsyncTest()
{ 
  return 1;
}

下面来分析该程序的执行流程:

 async Task<int>等于int

  1. Main()调用TestMain(),执行流转入TestMain();

这意味着我们在正常调用这两个函数时,他们是等效的。那么用async
Task<int>来修饰int目的是什么呢?

  1. www.9778.com,打印Start
  2. 调用GetValueAsync(),执行流转入GetValueAsync(),注意此处是同步调用;
  3. 执行Task.Run(),生成一个新的线程并执行,同时立即返回一个Task对象;
  4. 由于调用Task.Run()时,是以await作为修饰的,因此是一个异步调用,上下文环境保存第4步中返回的Task对象,在此处发生调用流阻塞,而当前的调用语句便是调用流阻塞点,于是发生调用流阻塞返回,执行流回到AysncCall()的GetValueAsync()处,并执行下一步

目的是为了让这个方法这样被调用 await
AsyncTest(),但直接这样调用,并不会开启线程,那这样费劲的修饰是不是就没什么意义了呢。

第5步之后就不好分析了,因为此时已经新建了一个线程用来执行后台线程,如果计算机速度够快,那么由于新建的线程代码中有一个Thread.Sleep;,因此线程会被阻塞,于是主线程会赶在新建的线程恢复执行之前打印End然后Console.ReadKey()在这里我假设发生的是这个情况,然后进入下面的步骤

当然不是,那什么时候会让 await AsyncTest()有意义呢?

  1. 新的线程恢复执行,打印0 1 2 3 4
    5
    ,线程执行结束,Task对象的IsCompleted变成true

我们接着往下看,修改AsyncTest如下。然后,此时再调用await
AsyncTest(),你会神奇的发现,依然没有卵用。。。

  1. 此时执行流跳转到调用流阻塞点,即从调用流阻塞点恢复执行流,发生了调用流阻塞异步完成跳转,于是打印Task End
  2. 程序执行流结束;

Excute方法正常执行,而AsyncTest内运行的线程,自己执行自己的。

仔细研究以上流程,可以发现async/await最重要的地方就是调用流阻塞点,这里的阻塞并不是阻塞的线程,而是阻塞的程序执行流。整个过程就像是一个食客走进一间饭馆点完菜,但是厨师说要等半个小时才做好,于是先给这个食客开了张单子让他先去外面逛一圈,等时间到了会通知他然后他再拿这张票来吃饭(调用流阻塞异步完成跳转);整个过程中这个食客并没有在饭馆做下来等,而是又去干了别的事情了。在这里,await就是用来指定调用流阻塞点的关键字,而async则是用来标识某个方法可以被调用流阻塞的关键字。

public static async void Excute()
 {
       Console.WriteLine(Thread.CurrentThread.GetHashCode() + " 开始 Excute " + DateTime.Now);
       await AsyncTest();
       Console.WriteLine(Thread.CurrentThread.GetHashCode() + " 结束 Excute " + DateTime.Now);
 }

 public static async Task<int> AsyncTest()
 {
        Task.Run(() =>
            {
                Console.WriteLine(Thread.CurrentThread.GetHashCode() + " Run1 " + DateTime.Now);
                Thread.Sleep(1000);
            });
            return 1;
 }

如果我们不使用await异步调用方法F的话,那么方法F将会被当成同步方法调用,即发生同步调用,这个时候执行流不会遇到调用流阻塞点,因此会直接往下执行,考虑上面的代码如果写成:

www.9778.com 16

 static async Task GetValueAsync() { Task.Run=> { Thread.Sleep; for(int i = 0; i < 5; ++i) { Console.Out.WriteLine(String.Format("From task : {0}", i)); } }); Console.Out.WriteLine("Task End"); }

别着急,我们稍作调整,在线程后面增加.GetAwaiter().GetResult()。这句话是干什么用的呢?是用来获取线程返回值的。

那么执行流不会在Task.Run()这里停下返回,而是直接“路过”这里,执行后面的语句,打印出Task
End
,然后和一般的程序一样返回。当然新的线程还是会被创建出来并执行,但是这种情况下的程序就不会去等Task.Run()完成了。在我的计算机上输出的结果如下:

这个逻辑是这样的,如果想要获取线程返回结果,就自然要等待线程结束。

StartTask EndEndFrom task : 0From task : 1From task : 2From task :
3From task : 4

运行一下,我们将看下面的结果。

根据C#的规定:若某个函数F的函数体中需要使用await关键字则该函数必须以async标记,此时F成为异步方法,于是,这会导致这样子的情况:需要使用await调用F的那个函数F’也必须以async标记

public static async Task<int> AsyncTest()
        {
            Task.Run(() =>
            {
                Console.WriteLine(Thread.CurrentThread.GetHashCode() + " Run1 " + DateTime.Now);
                Thread.Sleep(1000);
            }).GetAwaiter().GetResult();
            return 1;
        }

这个现象我称之为async传染

www.9778.com 17 

同时,C#又规定,Main函数不能够是异步方法,这意味着至少在Main函数中是不能够出现await异步调用的,进一步说明了任何的异步调用都是同步调用的子调用,而调用异步方法的那个方法我称之为病源隔断方法,因为在这里开始,不再会发生async传染。

但是,好像await
AsyncTest();还是没启作用。没错,事实就是,他真的不会起作用。。。

而在病源隔断方法中,一般会在其他操作完成之后去等待异步操作完成:

那么怎么才能让他起作用呢?

// 病源隔断方法void M(){ var task = F(); DoSomething(); if(task.IsCompleted) { // 类似Thread的join()方法 task.Wait(); }}// 异步方法async Task F() { await DoAsync();}

首先,我们定义一个普通函数,他的返回值是一个Task,然后我们得到Task后,运行它,再用await等待这个Task。

在上面的例子中,异步方法都是返回的Task,表示没有返回值。而如果要返回值的话,那么就简单地把Task换成Task<T>就行了,其中T是你的返回值的类型。

于是我们就得到这样的结果。

C#的Task<T>会自动和T完成装箱拆箱操作。也就是说如果异步方法F返回Task<int>对象,那么当异步方法完成的时候,它会自动变成int,整个过程由编译器完成:

 public static async void Excute()
        {
            Console.WriteLine(Thread.CurrentThread.GetHashCode() + " 开始 Excute " + DateTime.Now);
            var waitTask = AsyncTestRun();
            waitTask.Start();
            int i = await waitTask;
            Console.WriteLine(Thread.CurrentThread.GetHashCode() + " i " + i);
            Console.WriteLine(Thread.CurrentThread.GetHashCode() + " 结束 Excute " + DateTime.Now);
        }
        public static Task<int> AsyncTestRun()
        {
            Task<int> t = new Task<int>(() => {
                Console.WriteLine(Thread.CurrentThread.GetHashCode() + " Run1 " + DateTime.Now);
                Thread.Sleep(1000);
                return 100;
            });
            return t;
        }
void async M(){ int r = await F();}// 异步方法async Task<int> F() { await DoAsync(); return 0;}

www.9778.com 18  

这里说C#会自动完成Task<int>到T的装箱和拆箱事实上是不严谨的,因为编译器为我们隐藏了很多细节,这里只是“看起来”像是有这么个过程,但实质上并非如此。

如图,这样写await AsyncTest();就起作用了。

事实上异步方法的返回值声明声明的只是调用阻塞返回值,并不是异步方法执行完成后的真正返回值。造成这个事实的主要原因是存在调用阻塞返回真实方法返回两个返回值,前一个是“临时”的,而后一个是“执行完成后”的,因此我们可以认为Task<int>对应的是调用阻塞返回的返回值,而T这对应的是真实方法返回的返回值。

所以,还是那句话,await等待的是线程,不是函数。

我们可以把M进行改写,事实上编译器是为我们做了类似下面这样子的工作:

但在图里,我们发现很奇怪的一点,结束Excute也是线程3,而不是线程1。也就是说,Await会对线程进行优化。

void M(){ int r; Task<int> t = 获取调用F()时的调用阻塞点的Task<int>对象; t.OnCompleted += () => { r = t.Value; }; t.Wait();}

下面看下两组代码的对比,让我们就更清楚的了解下Await。

首先要明白的一点,就是async/await是不会主动创建线程的,创建线程的工作还是交给程序员来完成;async/await说白了就只是用来提供阻塞调用点的关键字而已。

第一组,使用await等待线程。

因此,如果我们要定义一个异步方法,那么至少要保证:

public static async void Excute()
{
   Console.WriteLine(Thread.CurrentThread.GetHashCode() + " 开始 Excute " + DateTime.Now);
   await SingleAwait(); 
   Console.WriteLine(Thread.CurrentThread.GetHashCode() + " 结束 Excute " + DateTime.Now);
}

public static async Task SingleAwait()
{
     Console.WriteLine(Thread.CurrentThread.GetHashCode() + " AwaitTest开始 " + DateTime.Now);
     await Task.Run(() =>
            {
                Console.WriteLine(Thread.CurrentThread.GetHashCode() + " Run1 " + DateTime.Now);
                Thread.Sleep(1000);
            });
            await Task.Run(() =>
            {
                Console.WriteLine(Thread.CurrentThread.GetHashCode() + " Run2 " + DateTime.Now);
                Thread.Sleep(1000);
            });
            Console.WriteLine(Thread.CurrentThread.GetHashCode() + " AwaitTest结束 " + DateTime.Now);
            return;
}
  1. 在异步方法的调用中会出现新的线程,无论调用层数有多深
  2. 一个新线程应该有且仅有一个阻塞调用点
  3. 异步方法嵌套调用的时候,
    每个嵌套调用的异步方法内部至少要调用一个异步方法或者await一个返回值为Task的同步方法。

www.9778.com 19

考虑以下代码:

 

async int M(){ return await F();}

第二组,使用等待线程结果,等待线程。

其中F()是一个异步方法,它返回的是Task<int>对象。

 public static async void Excute()
{
     Console.WriteLine(Thread.CurrentThread.GetHashCode() + " 开始 Excute " + DateTime.Now);
            await SingleNoAwait(); 
     Console.WriteLine(Thread.CurrentThread.GetHashCode() + " 结束 Excute " + DateTime.Now);
        }
public static async Task SingleNoAwait()
{
      Console.WriteLine(Thread.CurrentThread.GetHashCode() + " SingleNoAwait开始 " + DateTime.Now);
       Task.Run(() =>
        {
                Console.WriteLine(Thread.CurrentThread.GetHashCode() + " Run1 " + DateTime.Now);
                Thread.Sleep(1000);
            }).GetAwaiter().GetResult();
            Task.Run(() =>
            {
                Console.WriteLine(Thread.CurrentThread.GetHashCode() + " Run2 " + DateTime.Now);
                Thread.Sleep(1000);
            }).GetAwaiter().GetResult();
            Console.WriteLine(Thread.CurrentThread.GetHashCode() + " SingleNoAwait结束 " + DateTime.Now);
            return;
}

这段代码事实上等价于:

www.9778.com 20

async int M(){ int r = await F(); return r;}

可以明确的看到,第二组,线程重新回到了主线程1中,而第一组,已经被优化到了线程4中。

注意和

 

async Task<int> M(){ return F();}

 结语

区分。后面这段代码是一个同步方法,它只会返回F()的真实返回值。

await是一种很便捷的语法,他的确会让代码简洁一些,但他主动优化线程的功能,如果不了解就使用,可能会导致一些奇怪的BUG发生。

这也是官方为什么只提供了await调用服务的例子,因为,在程序内调用,await还是要了解后,再使用,才安全。

C#语法——委托,架构的血液

C#语法——元组类型

C#语法——泛型的多种应用


注:此文章为原创,欢迎转载,请在文章页面明显位置给出此文链接!
若您觉得这篇文章还不错,请点击下右下角的推荐,非常感谢!