重庆熊猫 Loading

.NET教程 - 进程 & 异步 & 并行 第二

基于Task的异步模式(TAP,Task-Based Asynchronous Pattern)

说明

处理异步工作,任务提供了比线程更好的抽象,任务会自动调度恰当数量的线程。但使用任务抽象的缺点是它颠倒了程序逻辑。为了解决这个问题出现了基于任务的异步模式/

注意:从C# 5.0开始才支持基于Task的异步模式。

基于任务的异步模式可以轻松的将同步的程序转为异步程序,底层由C#进行自动转换,将异步方法转为任务延续。

基于任务的异步模式实现

使用asyncawait关键字实现,在方法上使用async关键字修饰。并且方法只能返回Task、Task<T>、void之一。在需要等待的(创建新线程)任务使用await修饰。

注意:

  • 使用async修饰的方法不一定会创建新线程。因为方法内不一定有需要异步工作的代码。
  • await修饰的位置并不一定造成阻塞。
  • await位置和其他代码是并行执行的。

实例:

public async static Task<long?> GetPageLength()
{
    HttpClient client = new HttpClient();
    var httpMessage = await client.GetAsync("http://apress.com");
    return httpMessage.Content.Headers.ContentLength;
}

实例2:

public async Task<ViewResult> Index()
{
    List<string> output = new List<string>();
    await foreach (long? len in MyAsyncMethods.GetPageLengths(output,
    "apress.com", "microsoft.com", "amazon.com"))
    {
        output.Add($"Page length: { len}");
    }
    return View(output);
}

实例:无返回值

public static async void SomeTask2()
{
    await Task.Run(() => {

    });
}

实例:返回Task

public static async Task SomeTask2()
{
    await Task.Run(() => {

    });
}

实例:返回Task

public static async Task<string> SomeTask2()
{
    await Task.Run(() => {
        return "Panda666";
    });

    return "Panda666";
}

实例:在异步方法中调用其他异步方法

async Task<int> DoSomething()
{
    await DoSomethingAsync1();
    await DoSomethingAsync2();
    return await DoSomethingAsync3() + 1;
}

异步方法(async method)

说明

“异步方法”是指用async关键字修饰的方法(Asynchronous Function)。异步方法的方法名一般以Async结尾。调用异步方法一般在前面加上await关键字,表示等待该异步方法执行完成。使用await修饰后,返回的是异步方法Task<TResult>中的TResult类型,而不再是Task<TResult>类型。异步方法具有传染性。一个方法中如果由await调用,则这个方法必须使用async修饰。

static async Task<string> SomeMethod()
{
    await Task.Delay(TimeSpan.FromSeconds(2));
    return "Panda666.com";
}

异步方法支持的返回值

async methods can have the following return types:

返回类型 说明
Task<TResult> 用于异步方法( async method)返回指定值。最常用的返回类型,TResult是异步方法真正返回的类型。
Task 用于异步方法(async method)不返回值,但可以被等待。
void 用于异步方法(async method)不返回值,但不可以被等待。

注意:使用Task类型必须引入命名空间using System.Threading.Tasks;

注意:Task和void区别,都用于不返回值,但返回void的方法不可以被await等待,而返回Task就可以。

语法说明

方法使用async关键字修饰,异步方法内部使用await关键字修饰异步操作,在异步操作的地方使用await关键字修饰。异步函数内部必须至少一个await关键字,await之间不是同时运行的,而是按代码顺序运行的,即前一个await完成后才会运行后一个await。

//声明一个异步方法(async method)
async Task<int> GetLeisureHours()
{
    // Task.FromResult is a placeholder for actual work that returns a string.  
    var today = await Task.FromResult<string>(DateTime.Now.DayOfWeek.ToString());
    // The method then can process the result in some way.  
    int leisureHours;
    if (today.First() == 'S')
        leisureHours = 16;
    else
        leisureHours = 5;
    return leisureHours;
}

异步方法限制

  • 不可以在catch、finally、lock、unsafe代码块中使用await操作符。
  • 不可以对异步函数的参数使用ref、out修饰。

异步函数性能

  • 能用普通函数就不要使用异步函数,普通调用比async方法快40-~50倍,异步方法本质是转为一个带状态机的类,执行效率不高。
  • 可能会占用非常多的线程。

Async/await Main

在C# 7.1中加入了Async/await关键字对Main函数的支持。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace PandaTest
{
    public class Program
    {
        public async static Task Main(string[] args)
        {
            Console.WriteLine("Success");
        }
    }
}

异步任务转为非异步执行

如果同样的功能,既有同步方法,又有异步方法,最好优先使用异步方法。便于调试和提高运行效率。最新的.NET Core中的操作一般都支持异步操作,但有一些方法中没用办法使用异步方法,为此需要将异步任务转为同步执行的方式来运行。

注意:虽然可以将异步方法以非异步的方式执行,但容易发生死锁问题。

异步任务转为非异步执行主要方法如下:

  • 调用异步方法返回的Task实例对象的Wait()方法,适合于没用返回值的异步方法。
  • 调用异步方法返回的Task实例对象的Result属性,适合于由返回值的异步方法。

实例:使用Wait()方法

using System;
using System.Threading;
using System.Threading.Tasks;

namespace PandaTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            //调用异步方法
            string filePath = @"C:\Users\Administrator\Downloads\Text.txt";
            ReadAllTextAsync(filePath, CancellationToken.None).Wait();

            //wait
            Console.WriteLine("Success");
        }

        /// <summary>
        /// 读取文本文件(异步)
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns></returns>
        public async static Task ReadAllTextAsync(string filePath,CancellationToken cancellationToken)
        {
            string result = await File.ReadAllTextAsync(filePath, cancellationToken);
            Console.WriteLine(result);
        }
    }
}

实例:使用Result属性

using System;
using System.Threading;
using System.Threading.Tasks;

namespace PandaTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            //调用异步方法
            string filePath = @"C:\Users\Administrator\Downloads\Text.txt";
            //获得异步方法结果结果
            string result = ReadAllTextAsync(filePath, CancellationToken.None).Result;

            Console.WriteLine("文本结果");
            Console.WriteLine(result);
            //wait
            Console.WriteLine("Success");
        }

        /// <summary>
        /// 读取文本文件(异步)
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns></returns>
        public async static Task<string> ReadAllTextAsync(string filePath,CancellationToken cancellationToken)
        {
            return await File.ReadAllTextAsync(filePath, cancellationToken);
        }
    }
}

异步Lambda

说明

类似异步方法,转为Lambda表达式即可

注意:

async Lambda表达式和具有和async方法一样的限制

async Lambda表达式必须返回void、Task、Task的委托

实例:基本异步Lambda

//定义
Func<int,int,Task> t1 = async (int i, int j) =>
{
    await Task.Run(() => { });
};
//使用
t1(666, 888);//使用
t1(666, 888);

实例:异步委托注册到线程池

using System;
using System.Threading;
using System.Threading.Tasks;

namespace PandaTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            //线程池中使用异步Lambda
            ThreadPool.QueueUserWorkItem(async (obj) => {
                string filePath = @"C:\Users\Administrator\Downloads\Text.txt";
                await ReadAllTextAsync(filePath, CancellationToken.None);
            });
            
            Console.WriteLine("Success");
            Console.ReadKey();
        }

        /// <summary>
        /// 读取文本文件(异步)
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns></returns>
        public async static Task ReadAllTextAsync(string filePath,CancellationToken cancellationToken)
        {
            await Task.Delay(3000);
            string result = await File.ReadAllTextAsync(filePath, cancellationToken);
            Console.WriteLine(result);
        }
    }
}

异步方法的本质

await、async是“语法糖”,最终编译成“状态机调用”。即转为正常的方法调用,但是一个状态机,内部存在多个状态变化。

用await看似是“等待”,经过编译后,其实没有wait。async的方法会被C#编译器编译成一个类、会主要根据await调用进行切分为多个状态,对async方法的调用会被拆分为对MoveNext的调用。

await调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续的代码。可以使用Thread.CurrentThread.Ma
nagedThreadId获得当前线程Id来检测前后执行的线程是否是同一个线程。就像餐馆中的服务员,如果餐馆的生意并不好,就始终是一个服务员进行服务,没有必要让多个服务员进行服务。但如果是生意好,每个流程可能都是不同的服务员。

异步与yield

yield return不仅能够简化数据的返回,而且可以让数据处理“流水线化”,提升性能。在旧版C#中,async方法中不能用yield.从C#8.0开始,把返回值声明为IAsyncEnumerable(不要带Task),然后遍历的时候用await foreach()即可。

static async Task Main(string args)
{
    await foreach(var s in Test())
    {
        Console.WriteLine(s);
    }
}

//异步yield
static async IAsyncEnumerable<string> Test()
{
     yield "returnyzk";
	yield "returnyouzack";
}

异步方法注意

  • 异步方法并不是一定都在新线程中执行,如果任务执行的快有可能都在主线程上执行。

  • async方法中的异步方法如果没用使用await关键字修饰就会被当做同步方法执行。

public async static Task ReadAllTextAsync(string filePath,CancellationToken cancellationToken)
{
    //被当作同步方法,不会等待
    File.ReadAllTextAsync(filePath, cancellationToken);

    //被当作同步方法,不会等待
    Task.Delay(3000);
}
  • 异步方法不一定都使用async修饰,比如可以直接返回内部异步操作的结果,而不用再次封装。只甩手Task,不“拆完了再装”(异步方法本质仍是转为普通方法调用)。这样的优点是运行效率更高,不会造成线程浪费。
//定义不用async修饰的异步方法
public static Task ReadAllTextAsync(int flag, string filePath,CancellationToken cancellationToken)
        {
            if(flag == 0)
            {
                return File.ReadAllTextAsync(filePath, cancellationToken);
            }
            else if(flag == 1)
            {
                return File.ReadAllTextAsync(filePath, cancellationToken);
            }
            else
            {
                return File.ReadAllTextAsync(filePath, cancellationToken);
            }

        }
//在其他异步方法中使用
await ReadAllTextAsync(1,filePath, CancellationToken.None);

  • 返回Task的不一定都要标注async,标注async只是让我们更加方便的使用await,让编译器为我们代劳。
  • 如果一个异步方法只是对别的异步方法调用的转发,并没有太多复杂的逻辑(比如等待A的结果,再调用B;把A调用的返回值拿到内部做一些处理再返回),那么就可以去掉async。
  • 如果想在异步方法中暂停一段时间,不要用Thread.Sleep(),因为它会阻塞调用线程,而要用await Task.Delay()
  • async是提示编译器为异步方法中的await代码进行分段处理的,而一个异步方法是否修饰了async对于方法的调用者来讲没区别的,因此对于接口中的方法或者抽象方法不能修饰为async。

异步等待(待合并)

说明

.NET程序中的异步编程使用关键字:async 和 await

async关键字加在方法声明上,目的是使方法内的await关键字生效

C# 5(.NET 4.5)引入的语法糖
C# 7.1开始 Main方法也支持
C# 8.0开始,可以使用await foreach和await using

返回值:

如果async方法有返回值,应返回Task

如果没有返回值,应返回 Task

这些Task类型相当于 Future,用来在异步方法结束时通知主程序

注意

为了保持向后兼容,不要用 void 作为async方法的返回类型

async方法可以返回 void,但是这仅限于编写事件处理程序

async方法如果没有返回值,要返回Task,而不是void

规范

异步方法以Async结尾

具体原理

异步方法需要使用async修饰,即async方法

async方法需要返回Task或者Task类型

在async方法内调用异步操作时,需要加上await关键字,表示异步需要等待

当async方法执行到await位置时,当前线程进入等待状态,但不会阻塞

当await位置的异步任务执行完成后,会回到当前线程中继续执行后面的代码

桌面软件与async/await

一般情况下任何UI组件的交互都是在同一个UI线程中发生的

如果一个UI事件操作事件的执行需要大量时间会造成UI线程卡顿

用户会明显感到软件界面卡死

解决办法就是使用异步方式执行事件处理函数

//使用async修饰方法
private async void SomeControl_Click(object sender, EventArgs e)
{
    //内部对异步操作使用await修饰
    await xxxx.xxxAsync();
}
posted @ 2022-10-03 09:11  重庆熊猫  阅读(156)  评论(0编辑  收藏  举报