10.异步关键字async和await

10.CSharp各版本新功能和语法-CSharp5功能和语法-异步方法async和await


10.1 知识点

什么是同步和异步

同步和异步主要用于修饰方法。

同步方法:
当一个方法被调用时,调用者需要等待该方法执行完毕后返回才能继续执行。

异步方法:
当一个方法被调用时立即返回,并获取一个线程执行该方法内部的逻辑,调用者不用等待该方法执行完毕。

简单理解异步编程:
我们会把一些不需要立即得到结果且耗时的逻辑设置为异步执行,这样可以提高程序的运行效率,避免由于复杂逻辑带来的线程阻塞。

什么时候需要异步编程

需要处理的逻辑会严重影响主线程执行的流畅性时,我们需要使用异步编程。比如:

  1. 复杂逻辑计算时
  2. 网络下载、网络通讯
  3. 资源加载时
    等等

异步方法async和await

async和await一般需要配合Task进行使用。

  • async用于修饰函数、lambda表达式、匿名函数。
  • await用于在函数中和async配对使用,主要作用是等待某个逻辑结束。此时逻辑会返回函数外部继续执行,直到等待的内容执行结束后,再继续执行异步函数内部逻辑。在一个async异步函数中可以有多个await等待关键字。
  • 简单来说就是async修饰函数,await修饰Task,遇到await修饰Task回去执行主线程逻辑,等Task逻辑执行完再执行async修饰函数里的逻辑。

使用async修饰异步方法的注意事项

  1. 在异步方法中使用await关键字(不使用编译器会给出警告但不报错),否则异步方法会以同步方式执行。
  2. 异步方法名称建议以Async结尾。
  3. 异步方法的返回值只能是void、Task、Task<>。
  4. 异步方法中不能声明使用ref或out关键字修饰的变量。

使用await等待异步内容执行完毕的说明

遇到await关键字时:

  1. 异步方法将被挂起。
  2. 将控制权返回给调用者。
  3. 当await修饰内容异步执行结束后,继续通过调用者线程执行后面内容。

举例说明

普通异步方法
// 普通异步方法
public async void TestAsync()
{
    print("进入异步方法");
    await Task.Run(() =>
    {
        print("异步方法Task内");
        Thread.Sleep(5000); // 在异步任务中,线程暂停5秒钟(模拟耗时操作)
    });
    print("异步方法后面的逻辑");//这句会在五秒后才打印
}

TestAsync();
print("主线程逻辑执行");
进入异步方法
主线程逻辑执行
异步方法Task内
(等待 5 秒)
异步方法后面的逻辑
复杂逻辑计算
// 复杂逻辑计算(利用Task新开线程进行计算 计算完毕后再使用 比如复杂的寻路算法)
public async void CalcPathAsync(GameObject obj, Vector3 endPos)
{
    print("开始处理寻路逻辑");
    int value = 10;
    await Task.Run(() =>
    {
        print("处理复杂逻辑计算");
        Thread.Sleep(1000); // 处理复杂逻辑计算,模拟耗时操作
        value = 50;
        // 不能在多线程里访问 Unity 主线程场景中的对象
        // print(obj.transform.position);
    });
    print("寻路计算完毕 处理逻辑" + value);//这句会在一秒后才打印
    obj.transform.position = Vector3.zero;
}

CalcPathAsync(this.gameObject, Vector3.zero);
开始处理寻路逻辑       // 第一步,立即打印
处理复杂逻辑计算       // 第二步,后台线程开始后立即打印
(等待 1 秒)          // 后台线程 Sleep 1 秒
寻路计算完毕 处理逻辑50 // 第三步,恢复后打印,并更新 obj 位置
计时器
// 计时器
CancellationTokenSource cancellationTokenSource;

void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
        cancellationTokenSource.Cancel(); // 请求取消操作
}

public async void Timer()
{
    UnityWebRequest unityWebRequest = UnityWebRequest.Get(""); // 创建一个 UnityWebRequest 对象,用于发送网络请求
    cancellationTokenSource = new CancellationTokenSource(); // 创建一个 CancellationTokenSource 对象,用于取消任务
    int i = 0; // 初始化计数器 i

    while (!cancellationTokenSource.IsCancellationRequested) // 当 cancellationTokenSource 没有请求取消时,执行循环
    {
        print(i); // 打印当前计数器值
        await Task.Delay(1000); // 延迟等待 1000 毫秒(1秒)
        ++i; // 计数器增加1
    }
}

Timer();
print("主线程逻辑执行");
0
主线程逻辑执行
(等待 1 秒)
1
(等待 1 秒)
2
…(以此类推,直到按下空格键)
资源加载(Addressables的资源异步加载是可以使用async和await的)

更多示例

最基础的 Task.Run

当你有一段耗时操作,不想阻塞主线程,可以用 Task.Run 把它丢到线程池去执行:

using System;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        Console.WriteLine("主线程开始");

        // 开启一个后台任务
        Task.Run(() =>
        {
            // 模拟耗时操作
            Task.Delay(2000).Wait();
            Console.WriteLine("后台任务执行完毕");
        });

        Console.WriteLine("主线程继续干活");
        Console.ReadLine(); // 防止程序过早退出
    }
}

解释

  • Task.Run 接受一个委托,把它放在线程池里跑。
  • 主线程不会等待它完成,直接往下执行。
async/await:让异步更像同步

async 标记方法,返回 TaskTask<T>,内部使用 await 等待异步操作:

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Console.WriteLine("开始调用异步方法");
        await DoWorkAsync();
        Console.WriteLine("异步方法执行完毕");
    }

    // 返回 Task,表示无返回值的异步方法
    static async Task DoWorkAsync()
    {
        Console.WriteLine("    异步任务开始");
        await Task.Delay(1500);  // 异步等待,不阻塞线程
        Console.WriteLine("    异步任务结束");
    }
}

解释

  1. async Task:声明这是个异步方法。
  2. await Task.Delay(...):模拟异步等待,遇到 await 时方法会“挂起”,等延迟结束再继续。
  3. Main 中用 await DoWorkAsync(),主流程会在此“异步等待”,但实际线程可以去做别的事情。
带返回值的 Task

如果异步方法有返回值,用 Task<T>

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Console.WriteLine("计算结果中...");
        int result = await ComputeAsync(10, 20);
        Console.WriteLine($"计算结果:{result}");
    }

    // 返回 Task<int>,异步计算并返回 int
    static async Task<int> ComputeAsync(int a, int b)
    {
        await Task.Delay(1000);
        return a + b;
    }
}

解释

  • ComputeAsync 最终返回 int,所以方法签名是 async Task<int>
  • await ComputeAsync(...) 会取到 return 的值。
捕获异步错误

async 方法里,也可以用 try/catch 捕获异常:

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        try
        {
            await FaultyAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"捕获到异常:{ex.Message}");
        }
    }

    static async Task FaultyAsync()
    {
        await Task.Delay(500);
        throw new InvalidOperationException("出错啦!");
    }
}

解释

  • 异步方法抛出的异常,会在 await 的那一行重新抛出,使用 try/catch 就能捕获。
并行执行多个 Task

如果有多个相互独立的异步操作,可以同时启动,再用 Task.WhenAll 等待全部完成:

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Console.WriteLine("并行开始");
        
        // 同时启动三个异步任务
        Task t1 = Task.Delay(1000).ContinueWith(_ => Console.WriteLine("任务1 完成"));
        Task t2 = Task.Delay(1500).ContinueWith(_ => Console.WriteLine("任务2 完成"));
        Task t3 = Task.Delay(500).ContinueWith(_ => Console.WriteLine("任务3 完成"));

        // 等待全部任务完成
        await Task.WhenAll(t1, t2, t3);

        Console.WriteLine("所有任务都完成了");
    }
}

解释

  • Task.WhenAll 接收多个 Task,返回一个新 Task,代表它们都完成。
  • await Task.WhenAll(...) 会并行启动,性能更好。

Unity使用异步时的注意事项

  • Unity中大部分异步方法不支持异步关键字async和await,需要使用协同程序进行使用。
  • 虽然官方不支持,但是存在第三方工具(插件)可以让Unity内部的一些异步加载的方法支持异步关键字,如Unity3dAsyncAwaitUtil,网址是:https://github.com/svermeulen/Unity3dAsyncAwaitUtil。
  • 在使用到.Net库中提供的一些API时,可以考虑使用异步方法。例如:Web访问(HttpClient)、文件使用(StreamReader、StreamWriter、JsonSerializer、XmlReader、XmlWriter等等)、图像处理(BitmapEncoder、BitmapDecoder)等。一般.Net提供的API中方法名后面带有Async的方法都支持异步方法。
  • 最近有一个强大的,0GC的异步工具叫UniTask,可能是更好的选择

总结

异步编程async和await是一个比较重要的功能,我们可以利用它配合Task进行异步编程。

虽然Unity自带的一些异步加载原本是不支持异步方法关键字的,但是可以利用别人写好的第三方工具让它们支持。大家可以根据自己的需求选择性使用。


10.2 知识点代码

using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;

public class Lesson10_CSharp各版本新功能和语法_CSharp5功能和语法_异步方法async和await : MonoBehaviour
{
    void Start()
    {
        #region 知识点一 什么是同步和异步
        //同步和异步主要用于修饰方法
        //同步方法:
        //当一个方法被调用时,调用者需要等待该方法执行完毕后返回才能继续执行
        //异步方法:
        //当一个方法被调用时立即返回,并获取一个线程执行该方法内部的逻辑,调用者不用等待该方法执行完毕

        //简单理解异步编程
        //我们会把一些不需要立即得到结果且耗时的逻辑设置为异步执行,这样可以提高程序的运行效率
        //避免由于复杂逻辑带来的的线程阻塞
        #endregion

        #region 知识点二 什么时候需要异步编程
        //需要处理的逻辑会严重影响主线程执行的流畅性时
        //我们需要使用异步编程
        //比如:
        //1.复杂逻辑计算时
        //2.网络下载、网络通讯
        //3.资源加载时
        //等等
        #endregion

        #region 知识点三 异步方法async和await
        //async和await一般需要配合Task进行使用
        //async用于修饰函数、lambda表达式、匿名函数
        //await用于在函数中和async配对使用,主要作用是等待某个逻辑结束
        //此时逻辑会返回函数外部继续执行,直到等待的内容执行结束后,再继续执行异步函数内部逻辑
        //在一个async异步函数中可以有多个await等待关键字

        //使用async修饰异步方法
        //1.在异步方法中使用await关键字(不使用编译器会给出警告但不报错),否则异步方法会以同步方式执行
        //2.异步方法名称建议以Async结尾
        //3.异步方法的返回值只能是void、Task、Task<>
        //4.异步方法中不能声明使用ref或out关键字修饰的变量

        //使用await等待异步内容执行完毕(一般和Task配合使用)
        //遇到await关键字时
        //1.异步方法将被挂起
        //2.将控制权返回给调用者
        //3.当await修饰内容异步执行结束后,继续通过调用者线程执行后面内容

        //举例说明

        //1.普通异步方法
        TestAsync();
        print("主线程逻辑执行");


        //2.复杂逻辑计算(利用Task新开线程进行计算 计算完毕后再使用 比如复杂的寻路算法)
        CalcPathAsync(this.gameObject, Vector3.zero);

        //3.计时器
        Timer();
        print("主线程逻辑执行");

        //4.资源加载(Addressables的资源异步加载是可以使用async和await的)

        //注意:Unity中大部分异步方法是不支持异步关键字async和await的,我们只有使用协同程序进行使用
        //虽然官方 不支持 但是 存在第三方的工具(插件)可以让Unity内部的一些异步加载的方法 支持 异步关键字
        //https://github.com/svermeulen/Unity3dAsyncAwaitUtil

        //虽然Unity中的各种异步加载对异步方法支持不太好
        //但是当我们用到.Net 库中提供的一些API时,可以考虑使用异步方法
        //1.Web访问:HttpClient
        //2.文件使用:StreamReader、StreamWriter、JsonSerializer、XmlReader、XmlWriter等等
        //3.图像处理:BitmapEncoder、BitmapDecoder
        //一般.Net 提供的API中 方法名后面带有 Async的方法 都支持异步方法
        #endregion

        #region 总结 
        //异步编程async和await是一个比较重要的功能
        //我们可以利用它配合Task进行异步编程

        //虽然Unity自带的一些异步加载原本是不支持 异步方法关键字的
        //但是可以利用别人写好的第三方工具 让他们支持 大家可以根据自己的需求 选择性使用

        #endregion
    }

    #region 知识点三 异步方法async和await

    public async void TestAsync()
    {
        //1 打印"进入异步方法"
        print("进入异步方法");

        //2 使用Task.Run创建一个异步任务,并等待其完成(await关键字)
        await Task.Run(() =>
        {
            print("异步方法Task内");
            Thread.Sleep(5000); // 在异步任务中,线程暂停5秒钟(模拟耗时操作)
        });

        //3 打印"异步方法后面的逻辑"
        print("异步方法后面的逻辑");
    }


    public async void CalcPathAsync(GameObject obj, Vector3 endPos)
    {
        print("开始处理寻路逻辑");
        int value = 10;
        await Task.Run(() =>
        {
            print("处理复杂逻辑计算");

            //处理复杂逻辑计算 我这是通过 休眠来模拟 计算的复杂性
            Thread.Sleep(1000);
            value = 50;

            //是多线程 意味着我们不能在 多线程里 去访问 Unity主线程场景中的对象
            //这样写会报错
            //print(obj.transform.position);
        });

        print("寻路计算完毕 处理逻辑" + value);
        obj.transform.position = Vector3.zero;
    }



    CancellationTokenSource cancellationTokenSource;

    void Update()
    {
        // 检测是否按下了空格键
        if (Input.GetKeyDown(KeyCode.Space))
            // 请求取消操作
            cancellationTokenSource.Cancel();
    }


    public async void Timer()
    {
        // 创建一个 UnityWebRequest 对象,用于发送网络请求
        UnityWebRequest unityWebRequest = UnityWebRequest.Get("");

        // 创建一个 CancellationTokenSource 对象,用于取消任务
        cancellationTokenSource = new CancellationTokenSource();

        // 初始化计数器 i
        int i = 0;

        // 当 cancellationTokenSource 没有请求取消时,执行循环
        while (!cancellationTokenSource.IsCancellationRequested)
        {
            // 打印当前计数器值
            print(i);

            // 延迟等待 1000 毫秒(1秒)
            await Task.Delay(1000);

            // 计数器增加1
            ++i;
        }
    }




    #endregion
}

10.3 练习题

请导入第三方UnityAsyncAwaitUtil工具包到工程中,然后利用异步关键字async和await配合Resources中的异步加载方法LoadAsync加载并实例化出一个立方体预设体

方法一

public async void CreateCubeAsync1()
{
    print("进入直接异步创建Cube方法");

    ResourceRequest resourceRequest = Resources.LoadAsync<GameObject>("Cube");

    await resourceRequest;

    Instantiate(resourceRequest.asset);

    print("退出直接异步创建Cube方法");
}

CreateCubeAsync1();

方法二

public async Task CreateCubeAsync2()
{
    print("进入直接异步创建Cube方法2");

    ResourceRequest resourceRequest = Resources.LoadAsync<GameObject>("Cube");
    // 使用 Resources.LoadAsync 加载名为 "Cube" 的预制体资源,并创建一个 ResourceRequest 对象用于跟踪加载过程

    await resourceRequest;
    // 使用 await 关键字等待 ResourceRequest 完成,即加载完成

    Instantiate(resourceRequest.asset);
    // 在场景中实例化加载的 Cube 预制体资源,resourceRequest.asset 是加载完成的预制体资源

    print("退出直接异步创建Cube方法2");
}

public async void IndirectCreateCubeAsync()
{
    print("进入间接异步创建Cube方法");

    await CreateCubeAsync2();

    print("退出间接异步创建Cube方法");
}

IndirectCreateCubeAsync();

方法三


10.4 练习题代码

using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;

public class Lesson10_练习题 : MonoBehaviour
{
    void Start()
    {
        #region 练习题一 请导入上节课资料区中的 第三方UnityAsyncAwaitUtil工具包到工程中,然后利用异步关键字async和await配合Resources中的异步加载方法LoadAsync加载并实例化出一个立方体预设体


        CreateCubeAsync1();



        IndirectCreateCubeAsync();


        print("主线程执行");
        #endregion
    }
        

    public async void CreateCubeAsync1()
    {
        print("进入直接异步创建Cube方法");

        ResourceRequest resourceRequest = Resources.LoadAsync<GameObject>("Cube");

        await resourceRequest;

        Instantiate(resourceRequest.asset);

        print("退出直接异步创建Cube方法");
    }

    public async Task CreateCubeAsync2()
    {
        print("进入直接异步创建Cube方法2"); // 打印一条消息,指示进入异步创建 Cube 的方法

        ResourceRequest resourceRequest = Resources.LoadAsync<GameObject>("Cube");
        // 使用 Resources.LoadAsync 加载名为 "Cube" 的预制体资源,并创建一个 ResourceRequest 对象用于跟踪加载过程

        await resourceRequest;
        // 使用 await 关键字等待 ResourceRequest 完成,即加载完成

        Instantiate(resourceRequest.asset);
        // 在场景中实例化加载的 Cube 预制体资源,resourceRequest.asset 是加载完成的预制体资源

        print("退出直接异步创建Cube方法2"); // 打印一条消息,指示退出异步创建 Cube 的方法
    }

    public async void IndirectCreateCubeAsync()
    {
        print("进入间接异步创建Cube方法");

        await CreateCubeAsync2();

        print("退出间接异步创建Cube方法");
    }

}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏