C#中的async/await关键字

这对关键字可能是C#迄今为止争议最大的关键字了。这两个关键字可谓是让人又爱又恨了。爱的是这对关键字极大简化了C#异步调用代码的开发,恨的是对初学者非常不友好,对于没用过这对关键字的人来说,使得丈二和尚摸不着头脑。

下面我将以最直观的原理来讲解这两个关键字的作用。在讲解这个之前,先给大家说说协程的原理。协程指的是一个线程里面分配多个调度任务,然后由用户选择是否切换任务。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 协程类
public class MyCoroutine {
    private List<Func<bool>> m_tasks = new List<Func<bool>> ();
 
    // 添加一个任务至协程队列
    public void add (Func<bool> _f) {
        m_tasks.Add (_f);
    }
 
    // 协程主运行函数
    public void run () {
        while (m_tasks.Count > 0) {
            DateTime _dt = DateTime.Now.AddMilliseconds (10);
            for (int i = 0; i < m_tasks.Count; ++i) {
                if (m_tasks[i] ()) {
                    m_tasks.RemoveAt (i);
                    i--;
                }
            }
            if (DateTime.Now < _dt)
                Thread.Sleep (_dt - DateTime.Now);
        }
    }
}

上面的代码属于非常简单的协程实现代码了,里面有一个任务队列m_tasks,我们可以往任务队列里面添加任务,然后执行运行命令直到所有协程任务运行完毕为止。

接下来我们加入自己的调用代码,然后以协程的方式来执行任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Program {
    static void Main (string[] args) {
        MyCoroutine c = new MyCoroutine ();
        DateTime dt = DateTime.Now.AddSeconds (10);
        c.add (() => {
            if (DateTime.Now < dt)
                return false;
            Console.WriteLine ("hello world");
            return true;
        });
        c.run ();
        Console.ReadKey ();
    }
}

此处代码非常简单,创建协程类,然后添加一个任务。这个任务的目的是判断时间是否已经过去了10秒,如果过了10秒,那么打印“hello world”并退出。这儿判断是否超过10秒的原因是,此处模拟一些真实的任务场景,比如访问RESTful接口、解析数据等等,代表协程需要运行这么长的时间。此处返回值为false代表任务尚未运行完毕;如果返回true代表任务已经执行完成。这段代码运行的结果不出所料,等待10秒后打印了一个hello world。

下面再举一个例子,代表协程实际运行方式,这段代码写的较长,因为简化后的代码更加难懂。我就以原始含义来编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static void Main (string[] args) {
    MyCoroutine c = new MyCoroutine ();
    DateTime dt1 = DateTime.Now;
    int n1 = 0;
    c.add (() => {
        if (n1 == 0 && (DateTime.Now - dt1).TotalSeconds > 0) {
            Console.WriteLine ("task1 - 1");
            n1++;
            return false;
        } else if (n1 == 1 && (DateTime.Now - dt1).TotalSeconds > 2) {
            Console.WriteLine ("task1 - 2");
            n1++;
            return false;
        } else if (n1 == 2 && (DateTime.Now - dt1).TotalSeconds > 4) {
            Console.WriteLine ("task1 - 3");
            n1++;
            return false;
        } else if (n1 == 3) {
            Console.WriteLine ("task1 finish");
            return true;
        } else {
            return false;
        }
    });
    DateTime dt2 = DateTime.Now;
    int n2 = 0;
    c.add (() => {
        if (n2 == 0 && (DateTime.Now - dt1).TotalSeconds > 1) {
            Console.WriteLine ("task2 - 1");
            n2++;
            return false;
        } else if (n2 == 1 && (DateTime.Now - dt1).TotalSeconds > 3) {
            Console.WriteLine ("task2 - 2");
            n2++;
            return false;
        } else if (n2 == 2 && (DateTime.Now - dt1).TotalSeconds > 5) {
            Console.WriteLine ("task2 - 3");
            n2++;
            return false;
        } else if (n2 == 3) {
            Console.WriteLine ("task2 finish");
            return true;
        } else {
            return false;
        }
    });
    c.run ();
    Console.ReadKey ();
}

哈哈,代码看起来很长,两处c.add代表给协程任务队列添加了两个任务,这两个任务分别在奇数秒与偶数秒打印信息;分别打印三条后协程任务运行完毕。代码执行结束如下:

task1 - 1
task2 - 1
task1 - 2
task2 - 2
task1 - 3
task1 finish
task2 - 3
task2 finish

通过执行结果,很容易看出单线程内两个任务互相交替执行,这就是协程的核心工作方式。协程部分讲解完毕,下面说说这两个关键字到底是个啥。首先async,函数使用这个关键字修饰后,返回类型必须是Task或者Task。这个关键字的实际含义是,经过修饰后的函数为协程任务函数,也就是上面代码中c.add添加的lambda函数;返回类型中Task代表协程任务没有返回值;如果为T类型那么代表任务返回类型为T。(我上面的简单协程任务类里面就没有考虑到返回值的情况,理解意思就行)

另外,我上面的协程处理代码太过复杂,比如就一个简单的等待10秒然后打印hello world,需要写成那么复杂的形式。此处不使用Thread.Sleep的原因是,一旦这样等待了,线程将被休眠,阻塞其他协程的运行。那么如何解决呢?答案是await关键字。await关键字就是等待某个任务执行完毕,同时将后面所有的代码全部包装,等待任务执行完毕后再次执行:

1
2
3
4
5
6
7
8
9
static async Task func () {
    await Task.Run (() => { Thread.Sleep (10000); });
    Console.WriteLine ("hello world");
}
 
static void Main (string[] args) {
    func ().Wait ();
    Console.ReadKey ();
}

这段代码也就和上面的等待10秒后打印hello world的代码完全等价。下面我们再来模拟两个任务交替执行的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static async Task func1 () {
    Console.WriteLine ("task1 - 1");
    await Task.Run (() => { Thread.Sleep (2000); });
    Console.WriteLine ("task1 - 2");
    await Task.Run (() => { Thread.Sleep (2000); });
    Console.WriteLine ("task1 - 3");
    Console.WriteLine ("task1 finish");
}
 
static async Task func2 () {
    await Task.Run (() => { Thread.Sleep (1000); });
    Console.WriteLine ("task2 - 1");
    await Task.Run (() => { Thread.Sleep (2000); });
    Console.WriteLine ("task2 - 2");
    await Task.Run (() => { Thread.Sleep (2000); });
    Console.WriteLine ("task2 - 3");
    Console.WriteLine ("task2 finish");
}
 
static void Main (string[] args) {
    Task t1 = func1 ();
    Task t2 = func2 ();
    t1.Wait ();
    t2.Wait ();
    Console.ReadKey ();
}

代码看起来就很直观了,await一方面会暂停当前任务,另一方面不会像Thread.Sleep一样阻塞其他协程任务的运行。最后说说协程任务的调用。协程任务返回Task类型,只要返回了,那么就代表任务在运行了,此时可以自己去做其他事,当需要等待协程任务执行完毕时,调用Task的Wait方法即可;对于有返回值的任务类型,比如Task,这个协程任务将会返回一个string类型,当需要协程任务返回值时,访问Task的Result属性即可等待协程执行结束并返回任务结果。

发布者

fawdlstty

又一只萌萌哒程序猿~~

发表评论

电子邮件地址不会被公开。 必填项已用*标注