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新开线程进行计算 计算完毕后再使用 比如复杂的寻路算法)
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);
计时器
// 计时器
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("主线程逻辑执行");
资源加载(Addressables的资源异步加载是可以使用async和await的)

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的方法都支持异步方法。

总结

异步编程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

×

喜欢就点赞,疼爱就打赏