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已取消
*/
相关推荐
ytttr8731 小时前
隐马尔可夫模型(HMM)MATLAB实现范例
开发语言·算法·matlab
天远Date Lab1 小时前
Python实战:对接天远数据手机号码归属地API,实现精准用户分群与本地化运营
大数据·开发语言·python
listhi5201 小时前
基于Gabor纹理特征与K-means聚类的图像分割(Matlab实现)
开发语言·matlab
qq_433776421 小时前
【无标题】
开发语言·php
Davina_yu2 小时前
Windows 下升级 R 语言至最新版
开发语言·windows·r语言
阿珊和她的猫2 小时前
IIFE:JavaScript 中的立即调用函数表达式
开发语言·javascript·状态模式
listhi5202 小时前
卷积码编码和维特比译码的MATLAB仿真程序
开发语言·matlab
yuan199972 小时前
基于主成分分析(PCA)的故障诊断MATLAB仿真
开发语言·matlab
J_liaty2 小时前
Java版本演进:从JDK 8到JDK 21的特性革命与对比分析
java·开发语言·jdk
翔云 OCR API3 小时前
发票查验接口详细接收参数说明-C#语言集成完整示例-API高效财税管理方案
开发语言·c#