当前位置:   article > 正文

2023-08-04 Untiy进阶 C#知识补充4——C#5主要功能与语法

2023-08-04 Untiy进阶 C#知识补充4——C#5主要功能与语法


​ 注意:在此仅提及 Unity 开发中会用到的一些功能和特性,对于不适合在 Unity 中使用的内容会忽略。

一、概述
  • C# 5
    • 调用方信息特性(C# 进阶内容)
    • 异步方法 async 和 await
二、回顾——线程
  1. Unity 支持多线程
  2. Unity 中开启的多线程不能使用主线程中的对象
  3. Unity 中开启多线程后一定记住关闭
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;

public class Lesson4 : MonoBehaviour
{
    Thread t;
    
    // Start is called before the first frame update
    void Start()
    {
        t = new Thread(()=> {
            while (true) {
                print("123");
                Thread.Sleep(1000);
            }
        });
        t.Start(); // 开启线程
        print("主线程执行");
    }

    private void OnDestroy()
    {
        t.Abort(); // 关闭线程
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
三、线程池

​ 命名空间:System.Threading

​ 类名:ThreadPool

​ 在多线程的应用程序开发中,频繁地创建删除线程会带来性能消耗,产生内存垃圾。为了避免这种开销,C# 推出了线程池 ThreadPool 静态类。

​ ThreadPool 中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务。任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用。

​ 当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务;如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行。

​ 线程池能减少线程的创建,节省开销,可以减少 GC 垃圾回收的触发。

​ 线程池相当于就是一个专门装线程的缓存池(Unity小框架套课中有对缓存池的详细讲解)

  • 优点:节省开销,减少线程的创建,进而有效减少 GC 触发
  • 缺点:不能控制线程池中线程的执行顺序,也不能获取线程池内线程取消 / 异常 / 完成的通知
  1. 获取可用的工作线程数和 I/O 线程数

    int num1, num2;
    ThreadPool.GetAvailableThreads(out num1, out num2);
    
    print(num1); // 2000
    print(num2); // 200
    
    • 1
    • 2
    • 3
    • 4
    • 5
  2. 获取线程池中工作线程的最大数目和 I/O 线程的最大数目

    ThreadPool.GetMaxThreads(out num1, out num2);
    
    print(num1); // 2000
    print(num2); // 200
    
    • 1
    • 2
    • 3
    • 4
  3. 设置线程池中可以同时处于活动状态的工作线程的最大数目和 I/O 线程的最大数目

    // 大于次数的请求将保持排队状态,直到线程池线程变为可用
    // 更改成功返回true,失败返回false
    if(ThreadPool.SetMaxThreads(20, 20)) {
        print("更改成功");
    }
    
    ThreadPool.GetMaxThreads(out num1, out num2);
    
    print(num1); // 20
    print(num2); // 20
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  4. 获取线程池中工作线程的最小数目和 I/O 线程的最小数目

    ThreadPool.GetMinThreads(out num1, out num2);
    
    print(num1); // 16
    print(num2); // 16
    
    • 1
    • 2
    • 3
    • 4
  5. 设置工作线程的最小数目和 I/O 线程的最小数目

    if(ThreadPool.SetMinThreads(5, 5)) {
        print("设置成功");
    }
    
    ThreadPool.GetMinThreads(out num1, out num2);
    
    print(num1); // 5
    print(num2); // 5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  6. 将方法排入队列以便执行,当线程池中线程变得可用时执行

    • public static bool QueueUserWorkItem(WaitCallback callBack)
    • public static bool QueueUserWorkItem(WaitCallback callBack, object state)

    其中,state 为 callBack 的参数,不传则默认为 null。

    for (int i = 0; i < 10; i++) {
        ThreadPool.QueueUserWorkItem((obj) => {
            print("第" + obj + "个任务");
        }, i);
    }
    
    print("主线程执行");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    从运行结果可看出,控制线程池中线程的执行顺序不确定:

    image-20230619145519494
图1 线程池中线程的执行顺序
四、Task 任务类

​ 命名空间:System.Threading.Tasks

​ 类名:Task

​ Task 是在线程池基础上进行的改进,它拥有线程池的优点,同时解决了使用线程池不易控制的弊端。

​ 它是基于线程池的优点对线程的封装,可以让我们更方便高效的进行多线程开发,一个 Task 对象就是一个线程。

(一)创建无返回值的 Task

​ 本质上是从线程池中取出一个线程进行执行。

  1. 使用 new 传入委托函数

    Task t1 = new Task(() => {
        print("方式一创建");
    });
    
    t1.Start(); // 手动开启
    
    • 1
    • 2
    • 3
    • 4
    • 5
  2. 使用 Task 中的 Run 静态方法传入委托函数

    Task t2 = Task.Run(() => { // 直接开启
        print("方式二创建");
    });
    
    • 1
    • 2
    • 3
  3. 使用 Task.Factory 中的 StartNew 静态方法传入委托函数

    Task t3 = Task.Factory.StartNew(() => {
        print("方式三创建");
    });
    
    • 1
    • 2
    • 3

(二)创建有返回值的 Task

​ 在上述基础上添加返回类型的泛型即可。

  1. 使用 new 传入委托函数

    Task t1 = new Task<int>(() => {
        print("方式一创建");
        return 1;
    });
    
    t1.Start(); // 手动开启
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  2. 使用 Task 中的 Run 静态方法传入委托函数

    Task t2 = Task.Run<string>(() => { // 直接开启
        print("方式二创建");
        return "2";
    });
    
    • 1
    • 2
    • 3
    • 4
  3. 使用 Task.Factory 中的 StartNew 静态方法传入委托函数

    Task t3 = Task.Factory.StartNew<float>(() => {
        print("方式三创建");
        return 3.0f;
    });
    
    • 1
    • 2
    • 3
    • 4
  • 获取返回值

    print(t1.Result); // 1
    print(t2.Result); // 2
    print(t3.Result); // 3
    
    • 1
    • 2
    • 3

    注意:

    ​ Result 获取结果时会阻塞线程,如果 task 没有执行完成,会等待 task 执行完成获取到 Result 然后再执行后边的代码。

(三)同步执行 Task

​ 使用上述三种 Start、Run 和 StartNew 方法会在创建时异步启动 Task。

​ 如果需要同步执行,则只能使用 new Task 的方式并使用 RunSynchronously 方法。

  1. 异步执行

    Task t = new Task(()=> {
        Thread.Sleep(1000);
        print("哈哈哈");
    });
    t.Start();
    
    print("主线程执行");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    image-20230619151822601
    图2 异步执行结果
  2. 同步执行

    Task t = new Task(()=> {
        Thread.Sleep(1000);
        print("哈哈哈");
    });
    t.RunSynchronously();
    
    print("主线程执行");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    image-20230619151946027
    图3 同步执行结果

(四)阻塞 Task

  1. Wait:等待任务执行完毕,再执行后面的内容。

    Task t1 = Task.Run(() => {
        for (int i = 0; i < 5; i++) {
            print("t1:" + i);
        }
    });
    t1.Wait();
    
    print("主线程执行");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    当 t1 线程执行完毕后,才会执行主线程中的打印内容。

    image-20230619152500934
    图4 Wait执行结果
  2. WaitAny:传入任务中任意一个任务结束就继续执行。

    Task t1 = Task.Run(() => {
        for (int i = 0; i < 5; i++) {
            print("t1:" + i);
        }
    });
    Task t2 = Task.Run(() => {
        for (int i = 0; i < 50; i++) {
            print("t2:" + i);
        }
    });
    Task.WaitAny(t1, t2);
    
    print("主线程执行");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里,t1 执行完成后将会执行主线程的打印,但是 t2 线程仍继续执行。因为主线程与 t2 线程先后顺序无法控制,因此 t1 线程执行完成后没有立即打印主线程的内容。

    image-20230619153014365
    图5 WaitAny执行结果
  3. WaitAll:任务列表中所有任务执行结束就继续执行。

    t1、t2 线程都执行完成后,主线程才打印内容。

    image-20230619153154656
    图6 WaitAll执行结果

(五)延续 Task

  1. 传入任务完毕后再执行某任务

    • WhenAll + ContinueWith

      • WhenAll:创建一个任务,该任务将在所有提供的任务完成后完成。

        public static Task WhenAll(params Task[] tasks)

      • ContinueWith:创建在目标任务完成时异步执行的延续。

        public Task ContinueWith(Action<Task> continuationAction)

    Task.WhenAll(t1, t2).ContinueWith((t) => {
        print("一个新的任务开始了");
    });
    
    • 1
    • 2
    • 3
    • ContinueWhenAll:创建在一组指定任务完成时启动的延续任务。

      public Task ContinueWhenAll(Task[] tasks, Action<Task[]> continuationAction)

    Task.Factory.ContinueWhenAll(new Task[] { t1, t2 }, (t) => {
        print("一个新的任务开始了");
    });
    
    • 1
    • 2
    • 3
  2. 传入任务只要有一个执行完毕后再执行某任务

    • WhenAny + ContinueWith

      • WhenAny:创建一个任务,该任务将在提供的任何任务完成时完成。

        public static Task WhenAny(params Task[] tasks)

      • ContinueWith:创建在目标任务完成时异步执行的延续。

        public Task ContinueWith(Action<Task> continuationAction)

    Task.WhenAny(t1, t2).ContinueWith((t) => {
        print("一个新的任务开始了");
    });
    
    • 1
    • 2
    • 3
    • ContinueWhenAny:创建一个延续任务,该任务将在提供集中的任何任务完成后启动。

      public Task ContinueWhenAny(Task[] tasks, Action<Task[]> continuationAction)

    Task.Factory.ContinueWhenAny(new Task[] { t1, t2 }, (t) => {
        print("一个新的任务开始了");
    });
    
    • 1
    • 2
    • 3

(六)取消 Task

  1. 加入 bool 标识,控制线程内死循环的结束

    public class Lesson5 : MonoBehaviour
    {
        private bool isRuning = true; // 循环标识
    
        public Task t;
        
        // Start is called before the first frame update
        private void Start() 
        {
            t = Task.Run(() => {
                print("一个新的任务开始了");
                int i = 0;
                while (isRuning) { // 通过 isRuning 标识控制循环
                    print(i++);
                    Thread.Sleep(1000);
                }
            });
        }
        
        // Update is called once per frame
        void Update() 
        {
            if (Input.GetKeyDown(KeyCode.Space)) { // 按下空格停止线程
                isRuning = false;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
  2. CancellationTokenSource:取消令牌(标识)源类

    • 控制循环取消

      IsCancellationRequested:获取是否已请求取消此取消令牌源。

    public class Lesson5 : MonoBehaviour
    {
        public CancellationTokenSource c;
    
        public Task t;
        
        // Start is called before the first frame update
        private void Start() 
        {
            t = Task.Run(() => {
                print("一个新的任务开始了");
                int i = 0;
                while (!c.IsCancellationRequested) { // IsCancellationRequested 默认为 false
                    print(i++);
                    Thread.Sleep(1000);
                }
            });
        }
        
        // Update is called once per frame
        void Update() 
        {
            if (Input.GetKeyDown(KeyCode.Space)) { // 按下空格停止线程
                c.Cancel();                        // 使用 Cancel 方法停止
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 延迟取消

      CancelAfter:在指定的毫秒数后计划对此取消令牌源执行取消操作。

    public class Lesson5 : MonoBehaviour
    {
        public CancellationTokenSource c;
    
        public Task t;
        
        // Start is called before the first frame update
        private void Start() 
        {
            t = Task.Run(() => {
                print("一个新的任务开始了");
                int i = 0;
                while (!c.IsCancellationRequested) {
                    print(i++);
                    Thread.Sleep(1000);
                }
            });
        }
        
        // Update is called once per frame
        void Update() 
        {
            if (Input.GetKeyDown(KeyCode.Space)) {
                c.CancelAfter(5000); // 延迟 5s 取消
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 取消后执行逻辑

      Token.Register:注册取消此取消令牌时将调用的委托。

    public class Lesson5 : MonoBehaviour
    {
        public CancellationTokenSource c;
    
        public Task t;
        
        // Start is called before the first frame update
        private void Start() 
        {
            t = Task.Run(() => {
                print("一个新的任务开始了");
                int i = 0;
                while (!c.IsCancellationRequested) {
                    print(i++);
                    Thread.Sleep(1000);
                }
            });
            
            c.Token.Register(() => { print("任务取消了"); }); // 取消回调,线程被取消后将执行打印
        }
        
        // Update is called once per frame
        void Update() 
        {
            if (Input.GetKeyDown(KeyCode.Space)) {
                c.CancelAfter(5000); // 延迟 5s 取消
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

(七)小结

  1. Task 类是基于 Thread 的封装
  2. Task 类可以有返回值,Thread 没有返回值
  3. Task 类可以执行后续操作,Thread 没有这个功能
  4. Task 可以更加方便的取消任务,Thread 相对更加单一
  5. Task 具备 ThreadPool 线程池的优点,更节约性能
五、同步和异步

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

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

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

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

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

(一)async 关键字

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

​ async 用于修饰函数、lambda 表达式、匿名函数,表示该方法是一个异步方法。

public class Lesson6 : MonoBehaviour 
{
    void start() {
        Test(); // 打印 "123"
    }
    
    public async void Test() { // 方法中没有 await 关键字,则视为同步方法
        print("123");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

​ 上述代码声明了一个方法 Test(),在 void 前面添加关键字 async,表示该方法是异步的。在该方法内没有 await 关键字,因此编译器会发出警告,并将该方法默认视为同步方法。

​ 声明异步方法时,最好在函数名称后加上 Aysnc,以表示该方法为异步方法。

​ 下面总结了几点说明:

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

(二)await 关键字

​ await 用于在函数中和 async 配对使用,主要作用是等待某个逻辑结束。
​ 此时逻辑会返回函数外部继续执行,直到等待的内容执行结束后,再继续执行异步函数内部逻辑。

​ 在一个 async 异步函数中可以有多个 await 等待关键字。

​ 使用 await 等待异步内容执行完毕(一般和 Task 配合使用)遇到 await 关键字时:

  1. 异步方法将被挂起;
  2. 将控制权返回给调用者;
  3. 当 await 修饰内容异步执行结束后,继续通过调用者线程执行后面内容。
public class Lesson6 : MonoBehaviour 
{
    void start() {
        print("1");
        
        TestAsync();
        
        print("2");
    }
    
    public async void TestAsync()
    {
        print("3");      // 1
        
        await Task.Run(() => {    // 2
            Thread.Sleep(5000);
        });
        
        print("4"); // 3
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

​ 上述代码执行时,先打印主函数中的 “1”,然后进入 TestAsync() 异步函数,打印 “3”,遇到 await 关键字后,开启新的进程执行进程代码,TestAsync() 方法被挂起,执行主函数后面的代码,即接着打印 “2”。直到 Task 任务完成后才继续 TestAsync() 方法后面的代码,因此最后打印 “4”。

(三)举例

  1. 使用异步方法模拟复杂寻路计算:
public class Lesson6 : MonoBehaviour 
{
    void start() {
        // 利用Task新开线程进行计算 计算完毕后再使用 比如复杂的寻路算法
        CalcPathAsync(this.gameObject, Vector3.zero);
    }
    
    public async void CalcPathAsync(GameObject obj, Vector3 endPos) {
        print("开始处理寻路逻辑");
        
        int value = 10;
        await Task.Run(() => {
            Thread.Sleep(1000); // 处理复杂逻辑计算,通过休眠模拟计算的复杂性
            value = 50;
            // 不能在多线程里访问Unity主线程场景中的对象,这样写会报错
            // print(obj.transform.position); xxx
        });

        print("寻路计算完毕 处理逻辑" + value);
        obj.transform.position = Vector3.zero;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  1. 使用异步方法实现简单计时器:
public class Lesson6 : MonoBehaviour 
{
    CancellationTokenSource source;
    
    void start() {
        TimerAsync();
    }
    
    public async void TimerAsync()
    {
        source = new CancellationTokenSource();
        int i = 0;
        while (!source.IsCancellationRequested) {
            print(i);
            await Task.Delay(1000); // 延时 1000 ms
            ++i;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

(四)资源加载

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/138518
推荐阅读
相关标签
  

闽ICP备14008679号