对 C# 的关键字 async 和 await 的理解


对 C# 的关键字 async 和 await 的理解

1. 异步和同步

async,单词原意:异步。

在多线程编程中,通俗地讲,异步就是:在当前线程之外,另开一个线程,以执行一个相对独立的任务;当前线程不管新开线程是否执行完毕,继续执行自身任务或结束自身。

相反地,也是通俗地讲,同步就是:当前线程等待新开线程执行完毕,再继续执行自身任务【一个等待另一个的结束,在它结束之后,继续自身】。

背景知识点
    • 异步编程模型 (APM,Asynchronous Programming Model) ;
    • 基于事件的异步模式 (EAP,Event-based Asynchronous Pattern)
    • 基于任务的异步模式 (TAP, Task-based Asynchronous Pattern)

2. 限制条件

  • 被关键字 async 标记的函数,返回类型只能是: void、Task 或 Task、类似任务的类型、IAsyncEnumerable 或 IAsyncEnumerator
  • 被关键字 async 标记的函数,其【实现】需包含关键字 await (否则会出现警告,注意,并不是错误)。

3. 意义

一个函数,如果被关键字 async 标记,则意味着这个函数可能存在【具有异步的能力的】代码(做到了另开线程以完成某一任务的能力)。这些代码被关键字 await 标记出来。

调用者可以选择使用它的异步能力,或者放弃它的异步能力而改为同步执行(但保留了异步能力,第三方调用【调用者】时,仍然有同样的选择权利)

对于调用者,函数的异步能力,有时却会是一个大困扰,await标记的调用,避免了这样的麻烦。

4. 用法

以下代码示例,演示了一个函数通过 Http 请求以执行对指定临时数据的删除工作。它被关键字 async 标记,并且其【实现】包含了关键字 await 。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
...
    
// 被关键字 async 标记的函数
public async Task RemoveDataSetAsync(string deleteURL, string whereClause)
{
    if (string.IsNullOrEmpty(whereClause)) { return; }
    if (string.IsNullOrEmpty(deleteURL))
    {
	throw new Exception("deleteURL参数为空!");
    }

    var handler = new HttpClientHandler() 
    { 
        AutomaticDecompression = DecompressionMethods.None 
    };
    using (var httpclient = new HttpClient(handler))
    {
		httpclient.BaseAddress = new Uri(deleteURL);
		var content = new FormUrlEncodedContent(new Dictionary()
		{
			{"where", whereClause},
			{"f","json" }
		});

                // 关键字 await 标记的【具有异步的能力的】代码
		var response = await httpclient.PostAsync(deleteURL, content);
		string responseString = await response.Content.ReadAsStringAsync();
        
		Logger.Info($"DeleteURL返回状态值:{response.StatusCode}");
		if (!responseString.Contains("\"success\":true"))
                {
                    throw new Exception($"删除临时库数据失败!{responseString}");
                }
    }
}

所谓用法,即其他函数以什么样的方式调用它。

方式一
public class DeleteHelper
{
	public void Run()
	{
		Task.Run(new Action(RemoveAsync));
	}
    
	public async void RemoveAsync()
	{
		try
		{
			string whereClause = "1=1";
			string deleteUrl = "http://IP/LAYER/FeatureServer/0/deleteFeatures";
			await RemoveDataSetAsync(deleteUrl, whereClause);
		} catch { }
	}
    
    public async Task RemoveDataSetAsync(string deleteURL, string whereClause)
    {
        ...
    }
}

放弃异步能力,采用同步的方式,但为第三方保留了异步的能力。

【方法 RemoveAsync】对【方法 RemoveDataSetAsync】进行了调用。它将自身标记为了 async,并在调用 RemoveDataSetAsync 时,使用了关键字 await。这样的调用方式,正如 RemoveDataSetAsync 对 httpclient.PostAsync 的调用。此时,我们就会有疑问,这样的调用方式,意味着什么呢?

对具有异步能力的函数,使用关键字 await 进行调用它时,意味着放弃了使用它的异步能力,而是等待它另开线程的完成。此示例中,另开线程的工作在 httpclient.PostAsync 方法中提供。

方式二

那么,我们又将如何使用它提供的异步能力呢?请看下面的示例:

public class DeleteHelper
{
	public void Run()
	{
		Task.Run(new Action(RemoveAsync));
	}
    
	public void RemoveAsync()
	{
		try
		{
			string whereClause = "1=1";
			string deleteUrl = "http://IP/LAYER/FeatureServer/0/deleteFeatures";
                        // RemoveDataSet之前的代码
                        ...
			var _ = RemoveDataSetAsync(deleteUrl, whereClause);//放弃使用关键字 await 
                        // RemoveDataSet之后的代码
                        ...
		} catch { }
	}
    
    public async Task RemoveDataSetAsync(string deleteURL, string whereClause)
    {
                // 在第一个关键字 await 之前的代码
                ...
                // 关键字 await 标记的【具有异步的能力的】代码
		var response = await httpclient.PostAsync(deleteURL, content);
		string responseString = await response.Content.ReadAsStringAsync();
                ...
    }
}

在 RemoveAsync 方法中,不使用 关键字 await 对 RemoveDataSetAsync 时进行调用,则使用了 RemoveDataSetAsync 提供的异步能力,即不等待另开线程的执行完毕,而是直接运行了 RemoveDataSet 之后的代码。

此时,我们会有个疑问:具体线程是在什么时候放弃了【等待另开线程的执行完毕】?

答案是,在 RemoveDataSetAsync 方法中 第一个【关键字 await 】之前。即在第一个关键字 await 之前的代码,能被线程同步执行,然后线程立即转至执行 RemoveDataSet 之后的代码。这一点非常违背我们的直观,需要特别注意。如果不注意,就会错误的以为, RemoveDataSet之前的代码执行之后,直接转至执行 RemoveDataSet之后的代码,而忽略了在第一个关键字 await 之前的代码。

5. 总结

使用关键字 async 和 await ,除了对程序提供了性能提升之外,也对程序提供了异步调用能力的传递(被关键字 async 标记的新方法,对旧方法使用 await 标记的调用)。同时,使【代码阅读者】具有了更好的【对已有的具有多线程编程特性的代码】的理解能力。同时,使开发者能更好地、简便地进行多线程编程。

6. main 函数

附上 main 函数,以便大家理解本篇内容:

main 函数所在的主线程,与 RemoveAsync 所在的线程,不是同一个线程,因为进行了 Task.Run(new Action(RemoveAsync)) 调用。而这两个线程,又与 存在于 httpclient.PostAsync 中的线程不同。

internal class Program
{
	static void Main(string[] args)
	{
		try
		{
			DeleteHelper helper = new DeleteHelper();
			helper.Run();
		}
		catch
		{

		}
		finally
		{
			//进程保留
			Console.ReadLine();
		}
	}
}

因此,对于Task.Run,甚至可以是

public class DeleteHelper
{
    public void Run()
    {
	Task.Run(async () =>
        {
            try
            {
                string whereClause = "1=1";
                string deleteUrl = "http://IP/LAYER/FeatureServer/0/deleteFeatures";
                await RemoveDataSet(deleteUrl, whereClause);
             } catch { }
         });
    }
    
    public async Task RemoveDataSetAsync(string deleteURL, string whereClause)
    {
        ...
    }
}

另,要想一个函数原生具有异步能力,如何做到呢?

可以是以下方法,对返回值是 Task 方法,添加 await 标记,并标记函数为 async

public async void Run()
{
    Console.WriteLine("do some work here");

    await Task.Run(() =>
     {
         try
         {
         	Console.WriteLine("do some work here, too");
         }
         catch { }
     });

    Console.WriteLine("9.");
}

7. 结束语

如有错误,欢迎指正。