• 友链

  • 首页

  • 文章归档
h u a n b l o g
h u a n b l o g

欢

HI,Friend

04月
15
C#

异步方法

发表于 2022-04-15 • 字数统计 16363 • 被 1,437 人看爆

async/await特性结构

由三部分组成

  • 调用方法:使用异步方法,然后在异步方法(可能在相同的线程,也可能在不同的线程)执行其任务的时候继续执行。
  • 异步方法(async):该方法异步执行其工作,然后立即返回到调用方法。
  • await表达式:用于异步方法内部,指明需要异步执行的任务。一个异步方法可以包含任意多个await表达式,不过如果一个都不包含的话编译器会发出警告。

async和await特性整体结构.png

异步方法

异步方法在完成其工作之前即返回到调用方法,然后在调用方法继续执行的时候完成其工作

特点

  • 方法头中包含async方法修饰符。
  • 包含一个或多个await表达式,表示可以异步完成的任务。
  • 必须具备以下三种返回类型。第二种(Task)和第三种(Task)的返回对象表示将在未来完成的工作,调用方法和异步方法可以继续执行。
    • void
    • Task
    • Task
  • 异步方法的参数可以为任意类型任意数量,但不能为out或ref参数。
  • 异步方法的名称应该以Async为后缀。
  • 除了方法以外,Lambda表达式和匿名方法也可以作为异步对象。

例

//async关键字  Task<int>返回值
async Task<int> CountCharactersAsync(int id, string site) 
{
    Console.WriteLine("CountCharacters开始");
    WebClient wc = new WebClient();
    string result = await wc.DownloadStringTaskAsync(new Uri(site));        //await表达式
    return result.Length;
}

分析

1.异步方法在方法头中必须包含async关键字,且必须出现在返回类型之前。
2.该修饰符只是标识该方法包含一个或多个await表达式。也就是说,它本身并不能创建任何异步操作。
3.async关键字是一个上下文关键字,也就是说除了作为方法修饰符(或Lambda表达式修饰符、匿名方法修饰符)之外,async还可用作标识符。
4.返回类型必须是一下三种类型之一:

  • Task:如果调用方法要从调用中获取一个T类型的值,异步方法的返回类型就必须是Task。调用方法将通过读取Task的Result属性来获取这个T类型的值。下面的代码来自一个调用方法,阐明了这一点:
  Task<ints value = DoStuff.CalculateSumAsync(5, 6);
  ....
  Console.writeLine( "value: {0}",value.Result );
  • Task:如果调用方法不需要从异步方法中返回某个值,但需要检查异步方法的状态,那么异步方法可以返回一个Task类型的对象。这时,即使异步方法中出现了return语句,也不会返回任何东西。下面的代码同样来自调用方法:
  Task someTask = DoStuff.calculateSumAsync(S, 6);
....
  someTask.wait();
  • void:如果调用方法仅仅想执行异步方法,而不需要与它做任何进一步的交互时[这称为“调用并忘记”],异步方法可以返回void类型。这时,与上一种情况类似,即使异步方法中包含任何return语句,也不会返回任何东西。
  • 任何返回Task类型的异步方法其返回值必须为T类型或可以隐式转换为T的类型。如上面例子

实例1
使用返回Task对象的异步方法

static class DoAsyncStuff
{
    static private int GetSum(int i1, int i2)
    {
        return i1 + i2;
    }

    public static async Task<int> CalculateSumAsync(int i1,int i2)
    {
        //Task.Run()创建Task
        int sum = await Task.Run(() => GetSum(i1, i2));
        return sum;
    }
}



class Program
{

    static void Main(string[] args)
    {
        Task<int> value = DoAsyncStuff.CalculateSumAsync(5, 6);
        Console.WriteLine($"Value:{value.Result}");

        Console.ReadKey();
    }
}

实例2
使用返回Task对象的异步方法

static class DoAsyncStuff
{
    static private int GetSum(int i1, int i2)
    {
        return i1 + i2;
    }

    public static async Task CalculateSumAsync(int i1,int i2)
    {
        //Task.Run()创建Task
        int value = await Task.Run(() => GetSum(i1, i2));
        Console.WriteLine($"Value:{value}");
    }
}



class Program
{

    static void Main(string[] args)
    {
        Task someTask = DoAsyncStuff.CalculateSumAsync(5, 6);
        someTask.Wait();
        Console.ReadKey();
    }
}

实例3
使用"调用并忘记"的异步方法

using System.Threading;
using System.Threading.Tasks;
 
static class DoAsyncStuff
{
    static private int GetSum(int i1, int i2)
    {
        return i1 + i2;
    }

    public static async void CalculateSumAsync(int i1,int i2)
    {
        //Task.Run()创建Task
        int value = await Task.Run(() => GetSum(i1, i2));
        Console.WriteLine($"Value:{value}");
    }
}



class Program
{

    static void Main(string[] args)
    {
        DoAsyncStuff.CalculateSumAsync(5, 6);
        Thread.Sleep(200);
        Console.ReadKey();
    }
}

异步方法控制流

异步方法控制流.png

流程

  • 异步执行await表达式的空闲任务。
  • 当await表达式完成时,执行后续部分。后续部分本身也可能包含其他await表达式,这些表达式也将按照相同的方式处理,即异步执行await表达式,然后执行后续部分。
  • 当后续部分遇到return语句或到达方法末尾时,将:
    • 如果方法返回类型为void,控制流将退出。
    • 如果方法返回类型为Task,后续部分设置Task的属性并退出。如果返回类型为TaskcT>,后续部分还将设置Task对免的Recult属性

await表达式

指定一个异步执行的任务

语法

由await关键字和一个空闲对象(任务)组成
这个任务可能是一个Task对象,也可能不是
await task

一个空闲对象即是一个awaitable类型的实例。awaitable类型是指包含GetAwaiter方法的类型,该方法没有参数,返回一个称为awaiter类型的对象。awaiter类型包含以下成员:

  • bool IsCompleted{ get;}
  • void OnCompleted(Action);
  • 它还包含以下成员之一:
    • void GetResult();
    • T GetResult();(T为任意类型)

无需使用awaitable,使用Task即可,因为task就是awaitable类型

Task

异步操作

Run

Task.Run不同线程上运行方法

方法名

//Func<TReturn>委托参数
Task Run(Func<TReturn> func)

实例1


class MyClass
{
    public int Get10()      //与Func<int>兼容
    {
        return 10;
    }

    public async Task DoworkAsync()
    {
        Func<int> ten = new Func<int>(Get10);
        int a = await Task.Run(ten);
        int b = await Task.Run(new Func<int>(Get10));
        int c = await Task.Run(() => { return 10; });

        Console.WriteLine("{0} {1} {2}", a, b, c);
    }
}

class Program
{
    static void Main(string[] args)
    {
        Task t = (new MyClass()).DoworkAsync();
        t.Wait();   //等待所有task对象执行过程
        Console.ReadKey();
    }
}

分析

第一个实例(DoworkAsync方法的前两行)使用Get1o创建名为ten的Func<int>委托。然后在下一行将该委托用于Task.Run方法。
第二个实例在Task.Run方法的参数列表中创建Func<int``委托。 第三个实例没有使用Get10方法。而是使用了组成Get10方法的return语句,将其用于与Func`委托兼容的Lambda表达式。该Lambda表达式将隐式转换为该委托。

Task.Run重载的返回类型和方法名

返回类型方法名
TaskRun(Action action)
TaskRun(Action action, CancellationToken token)
Task<TResult>Run(Func<TResult> function)
Task<TResult>Run(Func<TResult> function, CancellationToken token)
TaskRun(Func<Task> function)
TaskRun(Func<Task> function, CancellationToken token)
Task<TResult>Run(Func<Task<TResult>> function)
Task<TResult>Run(Func<Task<TResult>> function, CancellationToken token)

可作为Task.Run方法第一个参数的委托类型

委托类型方法名含义
Actionvoid Action()不需要参数且无返回值的方法
Func<TResult>TResult Func()不需要参数返回TResult类型对象的方法
Func<Task>Task Func()不需要参数返回简单Task对象的方法
Func<Task<TRsult>>Task<TResult> Func()不需要参数返回Task<T>类型对象的方法

实例2

static class MyClass
{

    public static async Task DoworkAsync()
    {
        //Action
        await Task.Run(() => Console.WriteLine(5.ToString()));

        //TResult Func()
        Console.WriteLine((await Task.Run(() => 6)).ToString());

        //Task Func()
        await Task.Run(() => Task.Run(() => Console.WriteLine(7.ToString())));

        //Task<TResult> Func()
        int value = await Task.Run(() => Task.Run(() => 8));
        Console.WriteLine(value.ToString());
    }
}

class Program
{
    static void Main(string[] args)
    {
        Task t = MyClass.DoworkAsync();
        t.Wait();   //等待所有task对象执行过程
        Console.WriteLine("退出");
        Console.ReadKey();
    }
}

结果

5
6
7
8
退出

分析

第一个和第三个实例将await表达式用作语句。
第二个实例将await表达式用作writeline方法的参数。
第四个实例将await表达式用作赋值语句的右端。

使用Lambda表达式
上面有讲过,异步方法的实例3

static class MyClass
{

    private static int GetSum(int t1, int i2) {
        return i1 + i2;
    }

    public static async Task DoworkAsync()
    {
        int value = await Task.Run(() => GetSum(5, 6)));
        Console.WriteLine(value.ToString());
    }
}

class Program
{
    static void Main(string[] args)
    {
        Task t = MyClass.DoworkAsync();
        t.Wait();   //等待所有task对象执行过程
        Console.WriteLine("退出");
        Console.ReadKey();
    }
}

结果

11
退出

取消异步操作

using System.Threading.Tasks;命名空间
CancellationToken和CancellationTokenSource

规则

  • CancellationToken对象包含一个任务是否应被取消的信息。
  • 拥有CancellationToken对象的任务需要定期检查其令牌(token)状态。如果CancellationToken对象的IsCancellationRequested属性为true,任务需停止其操作并返回。CancellationToken是不可逆的,并且只能使用一次。也就是说,一旦IsCancellationRequested属性被设置为true,就不能更改了。
  • CancellationTokenSource对象创建可分配给不同任务的CancellationToken对象。任何CancellationTokenSource的对象都可以调用其Cancel方法,这会将CancellationToken的IsCancellationRequested属性设置为true。

实例
下面的代码展示了如何使用CancellationTokenSource和CancellationToken来实现取消操作。注意,该过程是协同的。即调用cancellationTokenSource的Cancel时,它本身并不会执行取消操作。而是会将CancellationToken的IsCancellationRequested属性设置为true。包含CancellationToken的代码负责检查该属性,并判断是否需要停止执行并返回。

class MyClass
{
    public async Task RunAsync(CancellationToken ct)
    {
        if (ct.IsCancellationRequested)
            //监控任务是否停止
            return;
        await Task.Run(() => CycleMethod(ct), ct);
    }

    void CycleMethod(CancellationToken ct)
    {
        Console.WriteLine("CycleMethod开始");
        const int max = 5;
        for(int  i = 0; i < max; i++)
        {
            if(ct.IsCancellationRequested)
            {
                //监控任务是否停止
                return;
            }
            Thread.Sleep(1000); //线程挂起时间
            Console.WriteLine("  {0} of {1} iterations completed", i + 1, max);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;

        MyClass mc = new MyClass();
        Task t = mc.RunAsync(token);

        //Thread.Sleep(3000);       //等待3秒
        //cts.Cancel();         //取消操作

        t.Wait();       //等待全部执行
        Console.WriteLine($"是否取消操作:{token.IsCancellationRequested}");
        Console.ReadKey();
    }
}

结果
保留注释

CycleMethod开始
1 of 5 iterations completed
2 of 5 iterations completed
3 of 5 iterations completed
4 of 5 iterations completed
5 of 5 iterations completed
是否取消操作:False

取消注释

CycleMethod开始
1 of 5 iterations completed
2 of 5 iterations completed
3 of 5 iterations completed
是否取消操作:True

异常处理

将await表达式放在try语句内,try...catch...finally结构

实例

static async Task BadAsync()
{
    try
    {
        await Task.Run(() => { throw new Exception(); });
    }
    catch
    {
        Console.WriteLine("Exception in BadAsync");
    }
}

static void Main(string[] args)
{
    Task t = BadAsync();
    t.Wait();
    Console.WriteLine("Task Status:{0}", t.Status); //表示 Task 的生命周期中的当前阶段。
    Console.WriteLine("Task IsFaulted:{0}", t.IsFaulted);   //获取 Task 是否由于未经处理异常的原因而完成。
    Console.ReadKey();
}

结果
运行时,会抛出异常,继续又可以运行

Exception in BadAsync
Task Status:RanToCompletion
Task IsFaulted:False

在调用方法中同步等待任务

调用方法可以调用任意多个异步方法并接收它们返回的Task对象

实例
下面的示例展示了其用法。在代码中,调用方法DoRun调用异步方法CountCharactersAsync并接收其返回的Taskint>。然后调用Task实例的wait方法,等待任务Task结束。等结束时再显示结果信息。

static class MyDownloadString
{
    public static void DoRun()
    {
        Task<int> t = CountCharacterAsync("https://www.lhuanblog.com");
        //等待任务结束
        t.Wait();
        Console.WriteLine("任务完成,返回:{0}", t.Result);
    }

    private static async Task<int> CountCharacterAsync(string site)
    {
        string result = await new WebClient().DownloadStringTaskAsync(new Uri(site));
        return result.Length;
    }
}

class Program
{
    static void Main(string[] args)
    {
        MyDownloadString.DoRun();

        Console.ReadKey();
    }
}

wait

用于等待任务完成
wait方法用于单一Task对象。而你也可以等待一组Task对象。
对于一组Task,可以等待所有任务都结束,也可以等待某一个任务结束。

实现这两个功能的是Task类中的两个静态方法:

  • WaitAll
  • WaitAny

没有返回值,直到满足条件再继续执行

实例2
下面代码,包含一个DoRun方法,两次调用一个异步方法并获取其返回两个Taskint>对象。然后,方法继续执行,检查任务是否完成并打印。方法最后会等待调用Console.Read,该方法等待并接收键盘输入的字符。
注释部分包含等待代码

class MyDownloadString
{
    Stopwatch sw = new Stopwatch();     //测量时间
    public void DoRun()
    {
        sw.Start();

        Task<int> t1 = CountCharacterAsync(1, "https://www.lhuanblog.com");
        Task<int> t2 = CountCharacterAsync(2, "https://www.microsoft.com");

        //Task<int>[] tasks = new Task<int>[] { t1, t2 };
        //Task.WaitAll(tasks);
        //Task.WaitAny(tasks);

        Console.WriteLine("Task 1 是否完成:{0}", t1.IsCompleted ? "是" : "否");
        Console.WriteLine("Task 2 是否完成:{0}", t2.IsCompleted ? "是" : "否");
        Console.Read();
    }

    private async Task<int> CountCharacterAsync(int id,string site)
    {
        string result = await new WebClient().DownloadStringTaskAsync(new Uri(site));
        Console.WriteLine("id: {0} 已完成 :  {1, 4} ms", id, sw.Elapsed.TotalMilliseconds);
        return result.Length;
    }
}

class Program
{
    static void Main(string[] args)
    {
        MyDownloadString ds = new MyDownloadString();
        ds.DoRun();

        Console.ReadKey();
    }
}

结果

Task 1 是否完成:否
Task 2 是否完成:否
id: 1 已完成 : 244.0255 ms
id: 2 已完成 : 1644.0659 ms

分析

可以看到没有一个是完成的(CountCharacterAsync未执行完),就直接进行下一步

如果把注释部分去掉

Task<int>[] tasks = new Task<int>[] { t1, t2 };
Task.WaitAll(tasks);
//Task.WaitAny(tasks);

结果

id: 1 已完成 : 243.06 ms
id: 2 已完成 : 3684.7435 ms
Task 1 是否完成:是
Task 2 是否完成:是

换一个

Task<int>[] tasks = new Task<int>[] { t1, t2 };
// Task.WaitAll(tasks);
Task.WaitAny(tasks);

结果

id: 1 已完成 : 225.2587 ms
Task 1 是否完成:是
Task 2 是否完成:否
id: 2 已完成 : 585.0498 ms

waitAll和WaitAny重载方法

方法名描述
void WaitAll(params Task[] tasks)等待所有任务完成
void WaitAll(params Task[] tasks, int millisecondsTimeout)等待所有任务完成。如果在超时时限内没有全部完成,则继续执行
void WaitAll(params Task[] tasks, CancelationToken token)等待所有任务完成,或CancellationToke发出了取消的信号
void WaitAll(params Task[] tasks, TimsSpan span)等待所有任务完成。如果在超时时限内没有全部完成,则继续执行
void WaitAll(params Task[] tasks, int millisecondsTimeout, CancelationToken token)等待所有任务完成,或CancellationToken发出了取消的信号。如果在超时时限内没有发生上述情况,则继续执行
void WaitAny(params Task[] tasks)等待任一个任务完成
void WaitAny(params Task[] tasks, int millisecondsTimeout)等待任一个任务完成。如果在超时时限内没有完成的,则继续执行
void WaitAny(params Task[] tasks, CancelationToken token)等待任一个任务完成,或CancellationToken发出了取消的信号
void WaitAny(params Task[] tasks, TimsSpan span)等待任一个任务完成。如果在超时时限内没有完成的,则继续执行
void WaitAny(params Task[] tasks, int millisecondsTimeout, CancelationToken token)等待任一个任务完成,或CancellationToken发出了取消的信号。如果在超时时限内没有发生上述情况,则继续执行

在异步方法中异步地等待任务

await表达式等待Task
通过Task.WhenAll和Task.WhenAny来实现

实例
下面展示Task.WhenAll方法,它异步地等待所有与之相关地Task完成,不会占用主线程时间

class MyDownloadString
{
    public void DoRun()
    {

        Task<int> t = CountCharacterAsync("https://www.lhuanblog.com", "https://www.microsoft.com");
       
        Console.WriteLine("DoRun:Task 是否完成:{0}", t.IsCompleted ? "是" : "否");
        Console.WriteLine("DoRun:Result:{0}", t.Result);
    }

    private async Task<int> CountCharacterAsync(string site1, string site2)
    {
        WebClient wc1 = new WebClient();
        WebClient wc2 = new WebClient();
        Task<string> t1 = wc1.DownloadStringTaskAsync(new Uri(site1));
        Task<string> t2 = wc2.DownloadStringTaskAsync(new Uri(site2));


        List<Task<string>> tasks = new List<Task<string>>();
        tasks.Add(t1);
        tasks.Add(t2);

        await Task.WhenAll(tasks);
        Console.WriteLine("   CCA: T1 是否完成:{0}", t1.IsCompleted ? "是" : "否");
        Console.WriteLine("   CCA: T2 是否完成:{0}", t2.IsCompleted ? "是" : "否");

        return t1.IsCompleted ? t1.Result.Length : t2.Result.Length;
    }
}

class Program
{
    static void Main(string[] args)
    {
        MyDownloadString ds = new MyDownloadString();
        ds.DoRun();

        Console.ReadKey();
    }
}

结果

DoRun:Task 是否完成:否
CCA: T1 是否完成:是
CCA: T2 是否完成:是
DoRun:Result:62902

将WhenAll改为WhenAny

结果

DoRun:Task 是否完成:否
CCA: T1 是否完成:是
CCA: T2 是否完成:否
DoRun:Result:62902

Task.Delay方法

Task.Delay方法创建一个Task对象,该对象将暂停其在线程中的处理,并在一定时间之后完成。
然而与Thread.Sleep阻塞线程不同的是,Task.Delay不会阻塞线程,线程可以继续处理其他工作。

实例

class Simple
{
    Stopwatch sw = new Stopwatch();

    public void DoRun()
    {
        Console.WriteLine("调用前");
        ShowDelayAsync();
        Console.WriteLine("调用后");

    }

    private async void ShowDelayAsync()
    {
        sw.Start();
        Console.WriteLine("   延迟前:{0}", sw.ElapsedMilliseconds);
        await Task.Delay(1000);
        Console.WriteLine("   延迟后:{0}", sw.ElapsedMilliseconds);
    }
}

class Program
{
    static void Main(string[] args)
    {
        Simple ds = new Simple();
        ds.DoRun();
        Console.ReadKey();
    }
}

结果

调用前
延迟前:0
调用后
延迟后:1024

Task.Delay方法重载

方法名描述
Task Delay(int millisecondDelay)在以毫秒表示的延迟时间到期后,返回完成的Task对象
Task Delay(TimeSpan span)在以 .NET TimeSpan对象表示的延迟时间到期后,返回完成的Task对象
Task Delay(int millisecondDelay, CancellationToken token)在以毫秒表示的延迟时间到期后,返回完成的Task对象。可通过取消令牌来取消该操作
Task Delay(TimeSpan span, CancellationToken token)在以 .NET TimeSpan对象表示的延迟时间到期后,返回完成的Task对象。可通过取消令牌来取消该操作

并行循环

Parallel.For循环和Parallel.ForEach循环
位于System.Threading.Tasks命名空间下

语法

//fromInclusive参数是迭代系列地第一个整数
//toExclusive参数是比迭代系列最后一个索引号大1的整数。与index < toExclusive一样
//body是接受单个输入参数的委托,body的代码在每次迭代中执行一次
public static ParallelLoopResult.For(int fromInclusive, int toExclusive, Action body);

实例1
如下代码是使用Parallel.For结构的例子。它从0到14迭代(记住实际的参数15超出了最大迭代索引)并且打印出迭代索引和索引的平方。该应用程序满足各个迭代之间是相互独立的条件。


 Parallel.For(0, 15, i => Console.WriteLine("{0}的平方是 {1}", i, i * i));

结果
每次输出结果可能不一样

1的平方是 1
2的平方是 4
0的平方是 0
4的平方是 16
3的平方是 9
11的平方是 121
12的平方是 144
13的平方是 169
14的平方是 196
8的平方是 64
9的平方是 81
10的平方是 100
7的平方是 49
6的平方是 36
5的平方是 25

实例2
另外一个并行循环结构是Parallel.ForEach方法。

string[] squares = new string[]
{
    "We","hold","these","truths","to","be","self-evident","that"
};
Parallel.ForEach(squares, i => Console.WriteLine(string.Format("{0}有{1}字符长度", i, i.Length)));

每次输出结果可能不一样
其它异步模式知识点

参考

  • 微软-TaskAPI
分享到:
其它异步模式
LINQ查询运算符
  • 文章目录
  • 站点概览
欢

网红 欢

你能抓到我么?

Email RSS
看爆 Top5
  • mac系统版本与Xcode版本有冲突 4,091次看爆
  • JAVA_HOME环境配置问题 3,741次看爆
  • AssetBundle使用 3,510次看爆
  • VSCode配置C++开发环境 3,264次看爆
  • Lua反射 3,141次看爆

Copyright © 2025 欢 粤ICP备2020105803号-1

由 Halo 强力驱动 · Theme by Sagiri · 站点地图