Unity 多线程编程笔记 _Thread详解

本文详细介绍了在Unity中使用多线程和线程池的基本原理和技术要点,包括线程创建、中止、状态获取及线程池的优缺点。探讨了Unity引擎的多线程限制和最佳实践,以及何时选择使用线程池。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

学习如何在Unity进行多线程开发之前,首先需要明确一点:Unity引擎采用了主循环结构,游戏逻辑的更新和渲染的更新在时间上都有特定的要求,大部分任务都在主线程中进行,引擎中绝大多数API都不是线程安全的,对于多线程并不友好,甚至可以说Unity根本没有多线程机制。如果引入了多线程,数据同步问题会大大增大开发的难度,当需要异步功能的时候,尽量使用Time-Slicing策略(unity中协程语法的本质就是Time-Slicing)。

Time-Slicing的核心思想是:如果一个任务不能在规定时间内(例如:50ms)执行完,那么为了
不阻塞主线程,这个任务应该让出主线程的控制权,使主线程可以处理其他任务。让出控制权意味
着停止执行当前任务,让主线程去执行其他任务,随后再回来继续执行没有执行完的任务。

当然不推荐并不代表不使用,也并不代表多线程编程在unity中没有优势,只是在我们使用前需要进行权衡和对比,是否必须使用多线程才能完成需求:如果不是,有没有其他更好的解决方案;如果是,那就放心大胆的使用多线程吧,多线程机制可以帮助实现多任务的并行运行解决延迟问题
下面枚举几个多线程的应用场景:

  • 逻辑复杂,计算量大,计算时间长,放在主线程会很卡的情况(英雄联盟中战争迷雾进行视野计算,就采用了多线程技术)
  • 进行数据下载,网络传输
  • 进行复杂且密集的I/O交互,读取外部传感器数据

Thread详解

在.net4.0之前的技术,在4.0之后更加推荐使用Task技术,.net4.5之后推出了更多的接口。

线程创建

创建并启动无参数的线程:

using System.Threading;
using UnityEngine;
using System;

public class Test01 : MonoBehaviour
{
   
    void Start()
    {
        //创建
        Thread thread01 = new Thread(ThreadTest);
        //启动
        thread01.Start();
        //使用匿名函数创建
        Thread thread02 = new Thread(()=>
        {
            int i = 0;
            while (i < 100)
            {
                Debug.LogFormat("Thread02 **** 时间:{0},i的值:{1}", DateTime.Now, i);
                i++;
            }
        });
        //启动
        thread02.Start();
    }
    public void ThreadTest()
    {
        int i = 0;
        while (i<100)
        {
            Debug.LogFormat("Thread01 **** 时间:{0},i的值:{1}", DateTime.Now, i);
            i++;
        }
    }

}

在这里插入图片描述
创建并启动带参数的线程:

using System.Threading;
using UnityEngine;
using System;
public class Test01 : MonoBehaviour
{ 
    void Start()
    {
        //创建
        Thread thread01 = new Thread(()=> {
            ThreadTest(50);
        });
        //启动
        thread01.Start();
    }
    public void ThreadTest(int count)
    {
        int i = 0;
        while (i< count)
        {
            Debug.LogFormat("Thread01 **** 时间:{0},i的值:{1}", DateTime.Now, i);
            i++;
        }
    }
}

在这里插入图片描述

Join && Sleep
using System.Threading;
using UnityEngine;
using System;
public class Test01 : MonoBehaviour
{ 
    void Start()
    {
        string res = "主线程不阻塞结果";
        //创建
        Thread thread01 = new Thread(()=> {
            Thread.Sleep(1000);   //延迟一秒
            res = ThreadTest(5);  //接收返回值
        });
        //启动
        thread01.Start();
        Debug.Log(res);   
        thread01.Join();   //阻塞主线程,在thread01没有执行完成之前不继续执行
        Debug.Log(res);

    }
    public string  ThreadTest(int count)
    {
        int i = 0;
        while (i< count)
        {
            Debug.LogFormat("Thread01 **** 时间:{0},i的值:{1}", DateTime.Now, i);
            i++;
        }
        return "主线程阻塞结果";
    }
}

在这里插入图片描述

中止线程 Abort

使用Abort可以手动终止线程,但这种方法并不推荐,它适用于当程序要关闭的时候进行调度,能够保证线程程序关闭,线程也被销毁。

  • Abort采用抛出异常的方式让线程销毁掉,这样是很不安全的,计算不准确,可能发生数值错误(比如用在线程内部用了lock语句,那么强制关闭线程将会导致lock失效,从而有可能影响计算结果.);
  • 此外它并不会马上停止,如果涉及非托管代码的调用,还需要等待非托管代码的处理结果。

注意:在unity中控制台不会自动捕捉线程中的错误,即使线程已经出现错误,也很难发现。最好使用Try_Catch 语句自行捕获。

using System.Threading;
using UnityEngine;
using System;
public class Test01 : MonoBehaviour
{ 
    void Start()
    {
        //创建
        Thread thread01 = new Thread(()=> {
            ThreadTest(5);  //接收返回值
        });
        //启动
        thread01.Start();
        thread01.Join();
        try
        {
            thread01.Abort();
        }
        catch (Exception e)
        {
            Debug.Log(e);
            throw;
        }
        finally
        {
            Debug.Log("Abort");
        }
    }
    public void  ThreadTest(int count)
    {
        int i = 0;
        while (i< count)
        {
            Debug.LogFormat("Thread01 **** 时间:{0},i的值:{1}", DateTime.Now, i);
            i++;
        }
    }
}

在这里插入图片描述

线程 暂停和运行
using System.Threading;
using UnityEngine;

public class Test01 : MonoBehaviour
{
    ThreadDemo demo;

    void Start()
    {
        demo= new ThreadDemo();
    }
    public void Pause()
    {
        demo.Pause();
    }
    public void ReStart()
    {
        demo.Start();
    }
}

public class ThreadDemo
{
    public ThreadDemo()
    {
        Thread t = new Thread(() =>
        {
            int i = 0;
            while (i < 20)
            {
                mr.WaitOne();
                Debug.Log("线程的ID:" + Thread.CurrentThread.ManagedThreadId);
                i++;
                Thread.Sleep(1000);
            }
        });
        t.IsBackground = false;
        t.Start();
        Debug.Log("线程开启");

    }
    //信号事件
    ManualResetEvent mr = new ManualResetEvent(true);
    public void Start()
    {
        Debug.Log("Start");
        mr.Set();
        
    }
    public void Pause()
    {
        Debug.Log("Pause");
        mr.Reset();
    }
}

在这里插入图片描述
上述代码中对于线程是否属于后台线程进行了设置,两者有什么区别呢?
如果应用程序想退出,执行ApplicationQuit,所有的前台进行必须已经执行完毕;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。

获取线程状态

在这里插入图片描述

namespace System.Threading
{
    [Flags]
    public enum ThreadState
    {
        Running = 0,
        StopRequested = 1,
        SuspendRequested = 2,
        Background = 4,
        Unstarted = 8,
        Stopped = 16,
        WaitSleepJoin = 32,
        Suspended = 64,
        AbortRequested = 128,
        Aborted = 256
    }
}
Thread t = new Thread(() =>
          {
              int i=0;
              while (true)
              { 
                  Debug.Log(i);
                  i++;
              }
          }
        );
        t.Start();
        ThreadState current= t.ThreadState;

ThreadPool

为什么使用线程池?(线程资源复用/自动管理)

通过上面的学习,当面对一个任务时可以采用开一个线程的方式来加速运算,然而当任务数量不断增加,自己创建线程的方式不再合理,原因如下:

  1. 在进程上开线程是需要消耗CPU时间,操作系统需要分配给新开的线程地址空间、栈空间、寄存器等,在线程结束的时候,操作系统又将这些东西回收(着同样需要消耗时间)。
  2. 创建了大量的线程,而这些线程不是每时每刻都在工作,在休眠状态等待事件发生花费大量时间,线程可能会进入休眠状态,只需要定期唤醒才能轮询更改或更新状态信息。 使用线程池,可以通过向应用程序提供由系统管理的工作线程池,来更有效地使用线程。

线程池线程是后台线程每个线程均使用默认的堆栈大小,以默认的优先级运行,并且位于多线程单元中。 一旦线程池中的线程完成任务,它将返回到等待线程队列中。 这时开始即可重用它。 通过这种重复使用,应用程序可以避免产生为每个任务创建新线程的开销。

线程池技术缺点
  1. ThreadPool类是一个静态的类,你不能也不需要生成它的对象。
  2. ThreadPool不支持线程的取消、完成、失败通知等交互性操作;
  3. ThreadPool不支持线程执行的先后次序;
线程池使用方式
  1. 基本设置
public static bool SetMaxThreads (int workerThreads, int completionPortThreads);  //最大
public static bool SetMinThreads (int workerThreads, int completionPortThreads);  //最小

设置线程池中最大/最小线程数目,超出的请求排队等待,有空线程的时候才执行。

workerThreads:线程池中辅助线程的最大数目。
completionPortThreads: 线程池中异步 I/O 线程的最大数目。

  1. 添加请求
    将方法加入请求进行排队。线程有空闲时执行。
public static bool QueueUserWorkItem (System.Threading.WaitCallback callBack);  //不带参数
public static bool QueueUserWorkItem (System.Threading.WaitCallback callBack, object state);  //带参数
什么时候不适合使用线程池?

有几种应用场景,其中适合创建并管理自己的线程,而非使用线程池线程:

  • 需要一个前台线程。
  • 需要具有特定优先级的线程。
  • 拥有会导致线程长时间阻塞的任务。 线程池具有最大线程数,因此大量被阻塞的线程池线程可能会阻止任务启动。
  • 需将线程放入单线程单元。 所有 ThreadPool 线程均位于多线程单元中。
  • 需具有与线程关联的稳定标识,或需将一个线程专用于一项任务。

参考资料

  1. Unity 为何要避免使用多线程
  2. 第一游戏学院视频教程
  3. C#官方资料
  4. 线程池
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值