C#异步编程

一、async/await特性

 在C#5之前,对于一个长耗时的操作,开发人员需要编写繁琐的回调、事件订阅以及应付错误,C#5推出了async/await特性,await语法可以在不阻塞当前线程的情况下等待某个操作完成。

二、异步函数

 异步函数是由async修饰符修饰的方法或者匿名函数,async修饰符是对函数中某个表达式使用await时的强制要求,在生成IL代码时async修饰符被省略了。只有异步函数才可以对await表达式使用await运算符,但是异步函数中也可以不包含await表达式,虽然这样async修饰符就没有了任何意义。

 await的主要作用是避免在等待长耗时操作时线程被阻塞。当执行到await时,代码会检查是否已得到执行结果(通常结果是否的),如果没有,它就会创建一个续延并立即返回,当await表达式操作完成后就继续执行该续延。

 这样开发人员可以像使用同步编程一样来编写异步代码:

    public partial class AsyncTestForm : Form
    {
        public string Result { get; set; } = "";
        private int index = 0;
        public AsyncTestForm()
        {
            InitializeComponent();
        }
        private void AsyncTestBtn_Click(object sender, EventArgs e)
        {
            //调用函数开始执行
            Result = string.Format("{0}:开始测试\n", ++index);
            QueryMsgByGetAsync(tbxUri.Text);
            //从异步方法中返回了
            Result += string.Format("{0}:结束测试函数\n", ++index);
        }
        public async void QueryMsgByGetAsync(string uri)
        {
            Result += string.Format("{0}:开始执行async函数\n", ++index);
            HttpClient client = new HttpClient
            {
                Timeout = TimeSpan.FromSeconds(30)
            };
            Result += string.Format("{0}:执行await表达式\n", ++index);
            HttpResponseMessage response = await client.GetAsync(uri);
            //await表达式完成后返回该处继续执行
            Result += string.Format("{0}:await表达式执行完成\n", ++index);
        }
        private void ShowRstBtn_Click(object sender, EventArgs e)
        {
            MessageBox.Show(Result);
        }
    }

在这里插入图片描述

 调用函数开始执行,程序进入async被调函数的await表达式处后,立即返回到调用函数,调用函数继续执行后续代码。当被调函数的await表达式执行完后,程序返回到该处继续执行。

 续延:指示某项操作完成之后执行什么操作。

三、await表达式

 await搭配的表达式必须是可等待的,可等待模式接下来讲解。除此之外,使用await表达式还有一些限制条件:

  • await必须用于async方法或匿名函数中
  • await用于async方法中的匿名函数时,该匿名函数也必须是异步的;
  • await不能用于非安全的上下文中;
  • await不能用于锁中。

 除此之外,对于async方法或者匿名函数也有使用要求:参数不能用ref或out修饰词修饰,异步返回时并不能获知ref或out参数的状态详情。

四、可等待模式

 可等待模式用于判断哪些类型可使用await运算符,是异步操作的定义基础。

 假设一个返回T类型的表达式使用await关键字,编译器会执行以下检查:

  • T必须具备一个无参的GetAwaiter()实例方法,或者T的扩展方法,该方法以类型T作为唯一参数。GetAwaiter方法的返回类型不能是woid,其返回类型称为awaiter类型。
  • awaiter类型必须实现System.Runtime.INotifyCompletion接口,该接口中只有一个方法:void
    Oncompleted(Action)。
  • awaiter类型必须具有一个可读的实例属性IsCompleted,其类型为bool
  • awaiter类型必须具有一个非泛型、无参数的实例方法GetResult。

 上述成员不必为public,但是这些方法必须能被调用await的async方法访问到。

public class Task
{
    public static YieldAwaitable Yield()
    {
        YieldAwaitable yieldAwaitable = new YieldAwaitable();
        //...
        return yieldAwaitable;
    }
}

public struct YieldAwaitable
{
    public YieldAwaiter GetAwaiter()
    {
        YieldAwaiter yieldAwaiter = new YieldAwaiter();
        //...
        return yieldAwaiter;
    }
    public struct YieldAwaiter : INotifyCompletion
    {
        public bool IsCompleted { get; }
        public void OnCompleted(Action action)
        {
            //...
        }
        public void GetResult()
        {
            //...
        }
    }
}

 顺便重提一下,可等待模式是语言层面的,编译器替我们做了这些隐藏起来的工作,我们不必再自己动手编写这些繁琐的代码。

五、异步模型

1.异步的本质

 在真实的异步模型中,续延并没有被传给异步操作,而是由异步操作发起并返回一个令牌,该令牌可供续延使用。该令牌代表正在执行的操作,该操作可能在返回调用方之前就已经执行完成了,也可能还在执行中。该令牌用于表达:在该操作完成前不能开始后续的操作。令牌通常是以Task或Task的形式出现的,但并非强制要求。

 一个简单的异步:

Task<string> task = client.GetStringAsync(uri);//创建令牌
string result = await task;//可以选择阻塞于此处,也可以将该令牌用于另一个续延
//也可以将两行代码合并:
string result = await client.GetStringAsync(uri);    await处理流程:

 await处理流程:
在这里插入图片描述

2.异步的上下文

 不同的执行环境会使用不同的上下文。异步模式的上下文比同步上下文要多,要想了解异步执行机制,首先需要了解同步上下文。SynchronizationContext类诞生于.net2.0,它负责在正确的线程中执行委托。对于更新UI,异步能通过实现SynchronizationContext类来保证await表达式能在UI线程中执行。

3.异步方法模型

 如图所示,异步有三个代码块和两个边界类型。
在这里插入图片描述

六、异步方法的返回类型

 C#5.0中异步函数可以返回3个类型:

  • void
  • Task
  • Task 或ValueTask

 在C#7.0中新增了自定义task类型,将在之后讲解。

1.void

 返回void被称为“调用并忘记”,它不反回任何值,仅仅是执行异步操作。

//将某文件复制到其他文件夹
string sourcePath = "G:\\Documents\\C# In Depth,4th Edition\\Chapter5 Summary.txt";
string destinationPath = "D:\\C# In Depth,4th Edition\\Chapter5 Summary.txt";
CopyFileAsync(sourcePath, destinationPath);

async void CopyFileAsync(string sourceFile, string destinationFile)
{
    FileStream sourceStream = new FileStream(
        path: sourceFile,
        mode: FileMode.Open,
        access: FileAccess.Read,
        share: FileShare.Read,
        bufferSize: 4096,
        options: FileOptions.Asynchronous | FileOptions.SequentialScan);
    FileStream destinationStream = new FileStream(
        path: destinationFile,
        mode: FileMode.CreateNew,
        access: FileAccess.Write,
        share: FileShare.None,
        bufferSize: 4096,
        options: FileOptions.Asynchronous | FileOptions.SequentialScan);
    await sourceStream.CopyToAsync(destinationStream);
    sourceStream.Close();
    destinationStream.Close();
}

2.Task

 返回Task和返回void类似,也不需要从异步方法中返回任何值,但是它可以检查异步方法的状态:

//将某文件复制到其他文件夹,复制完成后删除源文件
string sourcePath = "G:\\Documents\\C# In Depth,4th Edition\\Chapter5 Summary.txt";
string destinationPath = "D:\\C# In Depth,4th Edition\\Chapter5 Summary.txt";
Task task = CopyFileAsync(sourcePath, destinationPath);
//异步完成后执行后续代码
task.Wait();
File.Delete(sourcePath);

async Task CopyFileAsync(string sourceFile, string destinationFile)
{
    FileStream sourceStream = new FileStream(
        path: sourceFile,
        mode: FileMode.Open,
        access: FileAccess.Read,
        share: FileShare.Read,
        bufferSize: 4096,
        options: FileOptions.Asynchronous | FileOptions.SequentialScan);
    FileStream destinationStream = new FileStream(
        path: destinationFile,
        mode: FileMode.CreateNew,
        access: FileAccess.Write,
        share: FileShare.None,
        bufferSize: 4096,
        options: FileOptions.Asynchronous | FileOptions.SequentialScan);
    await sourceStream.CopyToAsync(destinationStream);
    sourceStream.Close();
    destinationStream.Close();
}

3.Task

 调用方法需要从调用中获取类型为TResult的值时,异步方法的返回类型必须是Task,任何返回Task的异步方法,其返回值必须是TResult类型,或者可以隐式转换成TResult的类型。

string filePath = "D:\\C# In Depth,4th Edition\\Chapter5 Summary.txt";
Task<string> task = ReaderFileAsync(filePath);
Console.WriteLine(task.Result);

async Task<string> ReaderFileAsync(string targetFile)
{
    StreamReader reader = new StreamReader(targetFile);
    return await reader.ReadToEndAsync();
}

七、异步方法的状态操作

 开发者可以取消或延迟异步方法,也可以在调用方法中同步地等待异步操作等。

1.取消异步

 一些异步方法允许用户终止执行,也可以通过CancellationToken和CancellationTokenSource类在自己的异步方法中实现该特性,它们的工作机制:

  • CancellationToken对象包含一个任务是否应被取消的信息;
  • 拥有CancellationToken对象的任务需要定期检查其令牌状态。如果CancellationToken对象的IsCancellationRequested属性为true,任务需要停止其操作并返回;
  • CancellationToken是不可逆的,一旦IsCancellationRequested属性被设置为true,就不能更改了;
  • CancellationTokenSource对象创建可分配给不同任务的CancellationToken对象,任何持有CancellationTokenSource的对象都可以调用其Cancel方法,将CancellationToken的IsCancellationRequested属性设置为true。
string filePath = "D:\\C# In Depth,4th Edition\\Chapter5 Summary.txt";
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;
Task<string> task = ReaderFileAsync(filePath, ct);
if (tokenCancell) cts.Cancel();
Console.WriteLine(task.Result);

async Task<string> ReaderFileAsync(string targetFile, CancellationToken token)
{
    if (token.IsCancellationRequested) return "token.IsCancellationRequested";
    StreamReader reader = new StreamReader(targetFile);
    return await reader.ReadToEndAsync();
}

2.延迟异步

 Task.Delay方法和Thread.Sleep方法不同,前者不会阻塞线程,线程可以继续处理其他工作。

string filePath = "D:\\C# In Depth,4th Edition\\Chapter5 Summary.txt";
Task<string> task = ReaderFileAsync(filePath);
Console.WriteLine(task.Result);

async Task CopyFileAsync(string targetFile)
{
    await Task.Delay(1000);
}

3.同步方法中同步等待异步操作完成

 使用Task.Wait()方法可以在同步方法中同步等待单一异步完成;使用Task.WaitAll()方法可以在同步方法中同步等待所有异步完成;使用Task.WaitAny()方法可以在同步方法中同步等待任一异步完成,其结果都是等待状态完成后执行后续操作。

4.异步方法中异步等待任务

 异步方法中使用await方法等待一个或多个其它异步操作时,可以使用Task.WhenAll或者Task.WhenAny来等待全部或者任意异步完成。

八、异步失败

 C#设计团队在语言层面提供了异步异常的操作支持。async/await的基础架构尽量让异步异常接近于同步异常相似的处理方式。

 以Task和Task为例,当异步操作失败时:

  • task的status属性变为Faulted,IsFaulted属性变为true,Exception属性返回AggregateExption,它包含导致任务失败的所有可能。
  • task的wait()方法和Result属性抛出一个AggregateExption。
    当取消异步操作时:
  • task的status属性变为Canceled,Exception属性返回AggregateExption。
  • 同失败一样,task的wait()方法和Result属性抛出一个AggregateExption。

九、关于自定义task类型

 一般不会需要用户去编写自定义类型的task,以后碰到需求会再返回这里将内容补上,这部分内容暂时不表。

  • 5
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穷的捡破烂儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值