文章目录
前言
学习如何在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
为什么使用线程池?(线程资源复用/自动管理)
通过上面的学习,当面对一个任务时可以采用开一个线程的方式来加速运算,然而当任务数量不断增加,自己创建线程的方式不再合理,原因如下:
- 在进程上开线程是需要消耗CPU时间,操作系统需要分配给新开的线程地址空间、栈空间、寄存器等,在线程结束的时候,操作系统又将这些东西回收(着同样需要消耗时间)。
- 创建了大量的线程,而这些线程不是每时每刻都在工作,在休眠状态等待事件发生花费大量时间,线程可能会进入休眠状态,只需要定期唤醒才能轮询更改或更新状态信息。 使用线程池,可以通过向应用程序提供由系统管理的工作线程池,来更有效地使用线程。
线程池线程是后台线程。 每个线程均使用默认的堆栈大小,以默认的优先级运行,并且位于多线程单元中。 一旦线程池中的线程完成任务,它将返回到等待线程队列中。 这时开始即可重用它。 通过这种重复使用,应用程序可以避免产生为每个任务创建新线程的开销。
线程池技术缺点
- ThreadPool类是一个静态的类,你不能也不需要生成它的对象。
- ThreadPool不支持线程的取消、完成、失败通知等交互性操作;
- ThreadPool不支持线程执行的先后次序;
线程池使用方式
- 基本设置
public static bool SetMaxThreads (int workerThreads, int completionPortThreads); //最大
public static bool SetMinThreads (int workerThreads, int completionPortThreads); //最小
设置线程池中最大/最小线程数目,超出的请求排队等待,有空线程的时候才执行。
workerThreads:线程池中辅助线程的最大数目。
completionPortThreads: 线程池中异步 I/O 线程的最大数目。
- 添加请求
将方法加入请求进行排队。线程有空闲时执行。
public static bool QueueUserWorkItem (System.Threading.WaitCallback callBack); //不带参数
public static bool QueueUserWorkItem (System.Threading.WaitCallback callBack, object state); //带参数
什么时候不适合使用线程池?
有几种应用场景,其中适合创建并管理自己的线程,而非使用线程池线程:
- 需要一个前台线程。
- 需要具有特定优先级的线程。
- 拥有会导致线程长时间阻塞的任务。 线程池具有最大线程数,因此大量被阻塞的线程池线程可能会阻止任务启动。
- 需将线程放入单线程单元。 所有 ThreadPool 线程均位于多线程单元中。
- 需具有与线程关联的稳定标识,或需将一个线程专用于一项任务。