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(); // 关闭线程
    }
}
三、线程池

​ 命名空间: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
    
  2. 获取线程池中工作线程的最大数目和 I/O 线程的最大数目

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

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

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

    if(ThreadPool.SetMinThreads(5, 5)) {
        print("设置成功");
    }
    
    ThreadPool.GetMinThreads(out num1, out num2);
    
    print(num1); // 5
    print(num2); // 5
    
  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 线程池中线程的执行顺序
四、Task 任务类

​ 命名空间:System.Threading.Tasks

​ 类名:Task

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

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

(一)创建无返回值的 Task

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

  1. 使用 new 传入委托函数

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

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

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

(二)创建有返回值的 Task

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

  1. 使用 new 传入委托函数

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

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

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

    print(t1.Result); // 1
    print(t2.Result); // 2
    print(t3.Result); // 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("主线程执行");
    
    图2 异步执行结果
  2. 同步执行

    Task t = new Task(()=> {
        Thread.Sleep(1000);
        print("哈哈哈");
    });
    t.RunSynchronously();
    
    print("主线程执行");
    
    图3 同步执行结果

(四)阻塞 Task

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

    Task t1 = Task.Run(() => {
        for (int i = 0; i < 5; i++) {
            print("t1:" + i);
        }
    });
    t1.Wait();
    
    print("主线程执行");
    

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

    图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("主线程执行");
    

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

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

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

    图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("一个新的任务开始了");
    });
    
    • ContinueWhenAll:创建在一组指定任务完成时启动的延续任务。

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

    Task.Factory.ContinueWhenAll(new Task[] { t1, t2 }, (t) => {
        print("一个新的任务开始了");
    });
    
  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("一个新的任务开始了");
    });
    
    • ContinueWhenAny:创建一个延续任务,该任务将在提供集中的任何任务完成后启动。

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

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

(六)取消 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;
            }
        }
    }
    
  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 方法停止
            }
        }
    }
    
    • 延迟取消

      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 取消
            }
        }
    }
    
    • 取消后执行逻辑

      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. 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");
    }
}

​ 上述代码声明了一个方法 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”,然后进入 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. 使用异步方法实现简单计时器:
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;
        }
    }
}

(四)资源加载

​ (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 的方法都支持异步方法。文章来源地址https://www.toymoban.com/news/detail-631574.html

到了这里,关于2023-08-04 Untiy进阶 C#知识补充4——C#5主要功能与语法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 2023-08-04 LeetCode每日一题(不同路径 III)

    点击跳转到题目位置 在二维网格 grid 上,有 4 种类型的方格: 1 表示起始方格。且只有一个起始方格。 2 表示结束方格,且只有一个结束方格。 0 表示我们可以走过的空方格。 -1 表示我们无法跨越的障碍。 返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方

    2024年02月14日
    浏览(40)
  • 2023-08-07 vmvare安装ubuntu18.04 ,安装VMware Tools后剪贴板无法共享问题

    一、安装VMware Tools死活不行,不能跟主机共享粘贴板,解决方法 二、实际操作,可以跟windows主机互相复制粘贴,非常莫名其妙。   三、参考文章 vmvare安装ubuntu后剪贴板无法共享问题_vmware 共享剪贴板_不许歪叽的博客-CSDN博客

    2024年02月13日
    浏览(46)
  • 2022-08-18 网工进阶(二十七) VRRP进阶知识-报文格式、定时器、状态机、主备(选举、切换、回切)、负载分担、监视、与MSTP结合应用

    2022-01-12 网工基础(二十)GRE原理与配置 VRRP原理与配置_鹅一只的博客-CSDN博客_gre的应用场景 相关命令 创建VRRP备份组并给备份组配置虚拟IP地址 配置路由器在备份组中的优先级 VRRP只有一种报文,即Advertisement报文,基于组播方式发送,因此只能在同一个广播域传递。 Advert

    2024年02月11日
    浏览(44)
  • C#进阶-实现邮箱收发功能

    在C#中,发送邮件是一项常见的任务,通常用于实现自动化通知、报警和与用户进行交互等场景。C#提供了多种发送邮件的方式,主要方式包括SMTP协议、POP3协议、IMAP协议、Exchange服务器等。使用这些方式,开发人员可以灵活地发送和接收邮件,满足各种应用场景的需求。 下表

    2024年04月15日
    浏览(30)
  • 【unity之c#专题篇】——进阶知识实践练习

    👨‍💻个人主页 :@元宇宙-秩沅 👨‍💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍💻 本文由 秩沅 原创 👨‍💻 收录于专栏 : unityc#专题篇习题 ⭐【Unityc#专题篇】之c#核心篇】 ⭐【Unityc#专题篇】之c#基础篇】 ⭐【Unity-c#专题篇】之c#入门篇) ⭐【Unityc#专题篇】

    2024年02月07日
    浏览(39)
  • 【Linux基础】Linux主要指令的详解(指令补充)

    语法: cp [选项] 源文件或目录 目标文件或目录 功能: 复制文件或目录 说明: cp指令用于复制文件或目录,如同时指定两个以上的文件或目录,且最后的目的地是一个已经存在的目录,则它会把前面指定的所有文件或目录复制到此目录中。若同时指定多个文件或目录,而最后的

    2024年02月03日
    浏览(38)
  • C#学习笔记--数据结构、泛型、委托事件等进阶知识点

    ArrayList 元素类型以Object类型存储,支持增删查改的数组容器。 因而存在装箱拆箱操作,谨慎使用。 ArrayList和数组区别? ArrayList使用不用说明固定长度,数组则需要 数组存储的是指定类型的,ArrayList是Object ArrayList存在装箱拆箱,数组不存在 ArrayList数组长度用Count获取 而数组

    2024年02月08日
    浏览(48)
  • 【C++进阶知识】04 - 函数默认实参、默认初始化、initializer_list

    默认实参需要注意以下几点: (1)函数默认实参的赋值应从右往左,否则编译报错,因为参数入栈应该从右往左。 (2)类外的默认实参会使类的非默认构造函数变成默认构造函数。 (3)如果在类中添加了该函数的该参数的默认实参,那么在类外再次定义该参数的默认实参

    2024年02月14日
    浏览(42)
  • 是否应该学习Qt作为主要编程语言C/C++的补充?

    如果您以C/C++作为主要编程语言,学习Qt是一个不错的选择。主要还是学习Qt的思想。 在初期阶段,您可以学习如何使用Qt设计界面。您可以使用Qt Designer拖拽控件,这样做比较直观。当然,您也可以手写代码实现界面。 Qt目前主要提供了两种UI实现方式:Widget和Qt Quick。 Widge

    2024年02月07日
    浏览(56)
  • 2023年,软件测试趋于饱和,如何从功能测试进阶到自动化测试?

    功能测试转成自动化测试,答案就三个字:“ 靠学习 ”。 学习自动化的方法无非是三种: 一、靠培训 在相对有氛围的学习环境中来学习自动化测试,这是一个较快学习的方法。 二、靠自学自动化教程(下方有视频资源推荐) 如果在职,不能全职学习,可以找一些自动化学

    2024年02月11日
    浏览(56)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包