C#异步编程详解

文章目录

初步

考虑到学习异步编程的人均非新手,这里就不举那个洗衣服喝咖啡的例子来徒增阅读成本了,直接从代码出发,看看异步函数和普通函数在使用上有何差异。

下面是写在顶级语句中的异步编程的最简单的例子,异步方法在建立后,并不会阻塞主线程,而是会和主线程并发工作,看上去就像开启了多线程一样。

cs 复制代码
async Task Hello()
{
    await Task.Delay(300);
    Console.WriteLine("Hello async");
}

Task hello = Hello();
for (int i = 0; i < 3; i++)
{
    Console.WriteLine($"主线程工作中... {i}");
    Thread.Sleep(300);
}
await hello;
/*输出如下
主线程工作中... 0
Hello async
主线程工作中... 1
主线程工作中... 2
*/

上述代码中

  • 【async】用于创建一个异步方法,当方法本身是void类型时,其返回值是一个Task;若方法本身有一个T类型的返回值,则被async修饰后,返回值变为Task<T>
  • 【await】是和async配套的阻塞标志,用于阻塞当前的Task。代码中共使用了两次。第一次是在Hello函数中,表示在等待300毫秒之后,再执行后面的WriteLine函数;第二次是在主线程结尾,表示等待hello运行完成之后,再继续执行主线程。

这段程序的执行过程是,调用Hello之后,Hello就开始执行了,由于遇到了await,故而Hello内部被阻塞。但Hello内部的await,只能阻塞Hello这个Task,主线程继续工作,开始跑循环。于此同时,Hello这个Task也在工作,并和主线程实现了异步输出。

当主线程的for循环结束后,又来到了主线程的await,而这个await的作用,是等待hello这个Task完成。在上述代码中,由于hello只延时300毫秒,而主线程延时了 3 × 300 3\times300 3×300毫秒,所以hello比主线程更早结束了。然而,如果取消主线程的延时,从而比Hello更快完成,此时若不await hello,那么就不会等待hello,直接退出,就看不到Hello async这个输出了。

需要注意,await只能用在异步方法中,即其函数主体必须用async关键字修饰。上述顶级代码中省略了Main函数,但由于使用了await,故而编译器生成的入口签名相当于

cs 复制代码
static async Task Main() { ... }

多任务异步并发

异步编程的主要优势,就是对任务的灵活调度。

例如,我们现在创建了许多个任务,记作 { t i } \{t_i\} {ti},常见的两种需求是

  1. 【WhenAll】当所有 t i t_i ti都运行完毕,再执行下一步。比如我下载了三个压缩包,每个压缩包都有独立的密码,必须得到所有密码,才能看到所有文件。
  2. 【WhenAny】任意 t i t_i ti运行完毕,即可执行下一步。比如我下载了一个压缩包,但使用三种算法来破解,任意算法破解成功,都可以执行下一步。

第一种需求相对来说比较简单,只需创建 { t i } \{t_i\} {ti}后挨个await就可以,WhenAll则可以一次性装入多个 t i t_i ti,WaitAll则更进一步,连await都不必写了。

下面随机生成0到100的整数作为密码,同时运行三个任务

cs 复制代码
async Task<int> guessInt(int id, int N)
{
    for (int i = 0; i < 100; i++)
    {
        await Task.Delay(1);    //假设实际计算耗时
        if (N != i)
            continue;
        Console.WriteLine($"任务{id}密码是{i}");
        return i;

    }
    Console.WriteLine($"任务{id}密码未破译");
    return -1;
}

Random rnd = new Random();

Task t1 = guessInt(1, rnd.Next(100));
Task t2 = guessInt(2, rnd.Next(100));
Task t3 = guessInt(3, rnd.Next(100));
//await Task.WhenAll(t1, t2, t3);
Task.WaitAll(t1, t2, t3);
//Task.WaitAll(t1, t2, t3);
/*输出如下
任务2密码是18
任务1密码是54
任务3密码是88
*/

第二种需求则相对复杂,需要在async任务中额外添加一些完成标记,而有了WhenAny,一切都变得简单多了,在语法上与WhenAll完全一致。下面同样是随机生成一个密码,但将破译过程拆分成三段,让这三段同时破译,效果如下(每次

cs 复制代码
async Task<int> guessInt(int id, int st, int N)
{
    for (int i = st; i < (st+100); i++)
    {
        await Task.Delay(1);
        if (N != i)
            continue;
        Console.WriteLine($"任务{id}破译成功,密码是{i}");
        return i;

    }
    Console.WriteLine($"任务{id}密码未破译");
    return -1;
}

Random rnd = new Random();

int pw = rnd.Next(300);
Task t1 = guessInt(1, 0, pw);
Task t2 = guessInt(2, 100, pw);
Task t3 = guessInt(3, 200, pw);
//await Task.WhenAny(t1, t2, t3);
Task.WaitAny(t1, t2, t3);
/*
任务2破译成功,密码是173
*/

取消任务

WaitAny的等待逻辑是,列表中任意一个任务运行完成,即不再等待,主线程继续工作。这里面有一个严重的问题:当等待结束,主线程继续工作后,那些尚未完成的任务仍在继续工作,这些工作有时候是没什么意义、徒废CPU的工作,需要取消。

而取消Task在C#中几乎采用通用的方案:CancellationTokenSource,示例如下

cs 复制代码
async Task<int> guessInt(int id, int st, int N, CancellationToken token)
{
    for (int i = st; i < (st+100); i++)
    {
        if (token.IsCancellationRequested)
        {
            Console.WriteLine($"任务{id}已取消");
            return -1;
        }
        await Task.Delay(1);
        if (N != i)
            continue;

        Console.WriteLine($"任务{id}破译成功,密码是{i}");
        return i;

    }
    Console.WriteLine($"任务{id}密码未破译");
    return -1;
}

Random rnd = new Random();

int pw = rnd.Next(300);
var cts = new CancellationTokenSource();
Task t1 = guessInt(1, 0, pw, cts.Token);
Task t2 = guessInt(2, 100, pw, cts.Token);
Task t3 = guessInt(3, 200, pw, cts.Token);
Task.WaitAny(t1, t2, t3);
cts.Cancel();
Task.WaitAll(t1, t2, t3);
/*
任务1破译成功,密码是90
任务2已取消
任务3已取消
*/
相关推荐
Whisper_Sy5 小时前
Flutter for OpenHarmony移动数据使用监管助手App实战 - 网络状态实现
android·java·开发语言·javascript·网络·flutter·php
Bony-6 小时前
Go语言垃圾回收机制详解与图解
开发语言·后端·golang
hmywillstronger6 小时前
【Rhino】【Python】 查询指定字段并cloud标注
开发语言·python
新缸中之脑6 小时前
Weave.js:开源实时白板库
开发语言·javascript·开源
我能坚持多久6 小时前
D16—C语言内功之数据在内存中的存储
c语言·开发语言
leo__5206 小时前
C#与三菱PLC串口通信源码实现(基于MC协议)
开发语言·c#
二十雨辰7 小时前
[python]-函数
开发语言·python
码农水水7 小时前
中国邮政Java面试被问:容器镜像的多阶段构建和优化
java·linux·开发语言·数据库·mysql·面试·php
福楠7 小时前
C++ STL | map、multimap
c语言·开发语言·数据结构·c++·算法
ytttr8737 小时前
地震数据频率波数域变换与去噪的MATLAB实现
开发语言·matlab