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小框架套课中有对缓存池的详细讲解)
获取可用的工作线程数和 I/O 线程数
int num1, num2;
ThreadPool.GetAvailableThreads(out num1, out num2);
print(num1); // 2000
print(num2); // 200
获取线程池中工作线程的最大数目和 I/O 线程的最大数目
ThreadPool.GetMaxThreads(out num1, out num2);
print(num1); // 2000
print(num2); // 200
设置线程池中可以同时处于活动状态的工作线程的最大数目和 I/O 线程的最大数目
// 大于次数的请求将保持排队状态,直到线程池线程变为可用
// 更改成功返回true,失败返回false
if(ThreadPool.SetMaxThreads(20, 20)) {
print("更改成功");
}
ThreadPool.GetMaxThreads(out num1, out num2);
print(num1); // 20
print(num2); // 20
获取线程池中工作线程的最小数目和 I/O 线程的最小数目
ThreadPool.GetMinThreads(out num1, out num2);
print(num1); // 16
print(num2); // 16
设置工作线程的最小数目和 I/O 线程的最小数目
if(ThreadPool.SetMinThreads(5, 5)) {
print("设置成功");
}
ThreadPool.GetMinThreads(out num1, out num2);
print(num1); // 5
print(num2); // 5
将方法排入队列以便执行,当线程池中线程变得可用时执行
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("主线程执行");
从运行结果可看出,控制线程池中线程的执行顺序不确定:
命名空间:System.Threading.Tasks
类名:Task
Task 是在线程池基础上进行的改进,它拥有线程池的优点,同时解决了使用线程池不易控制的弊端。
它是基于线程池的优点对线程的封装,可以让我们更方便高效的进行多线程开发,一个 Task 对象就是一个线程。
(一)创建无返回值的 Task
本质上是从线程池中取出一个线程进行执行。
使用 new 传入委托函数
Task t1 = new Task(() => {
print("方式一创建");
});
t1.Start(); // 手动开启
使用 Task 中的 Run 静态方法传入委托函数
Task t2 = Task.Run(() => { // 直接开启
print("方式二创建");
});
使用 Task.Factory 中的 StartNew 静态方法传入委托函数
Task t3 = Task.Factory.StartNew(() => {
print("方式三创建");
});
(二)创建有返回值的 Task
在上述基础上添加返回类型的泛型即可。
使用 new 传入委托函数
Task t1 = new Task<int>(() => {
print("方式一创建");
return 1;
});
t1.Start(); // 手动开启
使用 Task 中的 Run 静态方法传入委托函数
Task t2 = Task.Run<string>(() => { // 直接开启
print("方式二创建");
return "2";
});
使用 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 方法。
异步执行
Task t = new Task(()=> {
Thread.Sleep(1000);
print("哈哈哈");
});
t.Start();
print("主线程执行");
同步执行
Task t = new Task(()=> {
Thread.Sleep(1000);
print("哈哈哈");
});
t.RunSynchronously();
print("主线程执行");
(四)阻塞 Task
Wait
:等待任务执行完毕,再执行后面的内容。
Task t1 = Task.Run(() => {
for (int i = 0; i < 5; i++) {
print("t1:" + i);
}
});
t1.Wait();
print("主线程执行");
当 t1 线程执行完毕后,才会执行主线程中的打印内容。
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 线程执行完成后没有立即打印主线程的内容。
WaitAll
:任务列表中所有任务执行结束就继续执行。
t1、t2 线程都执行完成后,主线程才打印内容。
(五)延续 Task
传入任务完毕后再执行某任务
WhenAll + ContinueWith
WhenAll
:创建一个任务,该任务将在所有提供的任务完成后完成。
public static Task WhenAll(params Task[] tasks)
ContinueWith
:创建在目标任务完成时异步执行的延续。
public Task ContinueWith(Action
Task.WhenAll(t1, t2).ContinueWith((t) => {
print("一个新的任务开始了");
});
ContinueWhenAll
:创建在一组指定任务完成时启动的延续任务。
public Task ContinueWhenAll(Task[] tasks, Action
Task.Factory.ContinueWhenAll(new Task[] { t1, t2 }, (t) => {
print("一个新的任务开始了");
});
传入任务只要有一个执行完毕后再执行某任务
WhenAny + ContinueWith
WhenAny
:创建一个任务,该任务将在提供的任何任务完成时完成。
public static Task WhenAny(params Task[] tasks)
ContinueWith
:创建在目标任务完成时异步执行的延续。
public Task ContinueWith(Action
Task.WhenAny(t1, t2).ContinueWith((t) => {
print("一个新的任务开始了");
});
ContinueWhenAny
:创建一个延续任务,该任务将在提供集中的任何任务完成后启动。
public Task ContinueWhenAny(Task[] tasks, Action
Task.Factory.ContinueWhenAny(new Task[] { t1, t2 }, (t) => {
print("一个新的任务开始了");
});
(六)取消 Task
加入 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;
}
}
}
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 取消
}
}
}
(七)小结
同步和异步主要用于修饰方法
简单理解:把一些不需要立即得到结果且耗时的逻辑设置为异步执行,可以提高程序的运行效率,避免由于复杂逻辑带来的的线程阻塞。
需要处理的逻辑会严重影响主线程执行的流畅性时,需要使用异步编程,比如:
(一)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,以表示该方法为异步方法。
下面总结了几点说明:
(二)await 关键字
await 用于在函数中和 async 配对使用,主要作用是等待某个逻辑结束。
此时逻辑会返回函数外部继续执行,直到等待的内容执行结束后,再继续执行异步函数内部逻辑。
在一个 async 异步函数中可以有多个 await 等待关键字。
使用 await 等待异步内容执行完毕(一般和 Task 配合使用)遇到 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”。
(三)举例
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;
}
}
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 时,可以考虑使用异步方法
一般 .Net 提供的 API 中,方法名后面带有 Async 的方法都支持异步方法。