引入:网络编程基础认识
1.了解操作系统的分时操作:
操作系统将时间划分为很多个片段,尽可能均匀地分配给正在执行的线程
获得时间片的进程得以运行,其他则在等待
CPU在这些进程上来回切换,频密,让人感觉多个进程在同时执行
2.概念认识:
(1)进程是程序的边界,程序与程序间以进程为隔离 靠线程执行程序,
线程是进程内部的组成单位,指向方法,执行完毕释放线程
(2)进程是资源分配的单位,
线程是执行的单位,是进程中的一个执行单元,是CPU分配时间片的单位
(3)线程的本地存储器:TLS
线程的本地存储器(Thread Local Storage, TLS)是一种用于在多线程程序中为每个线程提供独立存储区域的机制。它允许每个线程有自己独立的变量副本,这些变量在不同线程之间不会互相干扰,从而避免了数据竞争和同步问题。
(4)Socket编程:使多个进程间能够进行通信,网络编程中,Socket 是一种用于网络通信的基本概念和接口。它是一种抽象层,提供了不同网络协议(如 TCP/IP 和 UDP)的通信机制,允许应用程序在网络上进行数据交换。Socket 允许程序与其他网络节点(例如计算机、服务器等)进行连接和数据传输。
3.可能疑问:
1.辅助线程和日常使用的异步async,协程 Coroutine的区别:
这里要明确的是异步和携程 主要依赖于主线程,并不会自动创建新的线程。
一.线程
1.对线程的印象
Unity默认只有一条主线程,在主线程内运行Unity生命周期函数,但是Unity是支持多线程的。
日常生活中我们可能会遇到这样一种情况:
在高中的时候,我们一般都会遇到在食堂的一个窗口前排队买饭的情景,现假设只有一个窗口可以供同学们打饭,每位同学依次排队打一 ~ 五单位的饭,但是,如果出现了一位同学,假如他的饭量有亿点大,要打100个单位的饭,那他后面的同学岂不是都饥肠辘辘了?这时,我们可以多开几个窗口,让一个窗口专门去处理这位大胃王的饭,这样一来全体同学的打饭效率就得到了提升
线程的作用可以类比上面的例子。
2.为什么使用线程
当主线程内运行计算量大的耗时计算时,移至线程计算
使用多线程减少程序的运行时间
3.线程的优缺点
优点:
并行执行,合理利用CPU资源
相同程序的线程共享堆内存
缺点: | 解决方案
频繁创建、销毁线程增加性能开销 |(使用线程池)
访问共享资源可能造成冲突 |(使用lock来同步线程)
辅助线程不能访问Unity API |(“外包”给委托)
4.线程的创建
说了这么多,我们直接了解如何创建线程吧 @\/@
我们先定义到Thread内部去看看Thread内置的方法
可以看到接收的是一个委托类型的参数,且该委托的参数为无参或object类型
我们可以利用一个无参函数,也可以通过匿名函数来创建线程
实验一下,在这里我写了三种形式,代码里的System.Sleep()表示令调用其的线程休眠指定时间
在编辑器内挂载该脚本并运行得到结果如下:
5.信号灯(ManualResetEvent :手动重置事件)
ManualResetEvent 常被称为信号灯,用来控制目标线程的暂停与运行,
首先创建一个信号灯,参数设置为false,此时所有signal控制线程初始都会暂停
//创建信号灯
ManualResetEvent signal=new ManualResetEvent(false);
//false:初始处于关闭状态 ;true:初始处于通行状态
这里我使用Unity内置的OnGUI()函数来进行实验,Set表示通行 ReSet表示禁止通行
编辑器内运行结果:一开始所有线程都是处于禁止通行状态,按下继续Button,线程开始运行,
注意:此时如果再次按下暂停不会有效果,原因是 ManualResetEvent的自身特性 ,在调用一次Set()后将允许恢复所有被阻塞线程。需手动在调用WaitOne()之后调用Reset()重置信号量状态为非终止,然后再次调用WaitOne()的时候才能继续阻塞线程,反之则不阻塞。
6.线程池(线程缺点解决方案):
前面提到线程的缺点之一:频繁创建、销毁线程增加性能开销
我们可以采用线程池技术来缓解这一限制:
频繁创建和销毁线程时使用,能有效减少以及系统的开销
ThreadPool.QueueUserWorkItem(obj =>
{
ThreadpollTest();
});
7.线程同步(线程缺点解决方案):
继续说线程的缺点之二:访问共享资源可能造成冲突
我们采用lock来处理这样的问题,让多个线程在调用共享资源的时候也按照一定的先后顺序来执行。
关于lock:
(1)什么时候需要考虑用锁:
多线程 多个线程同时访问共享资源时 需要考虑用锁
(2)lock有什么用:
lock
关键字用于在多线程环境下同步对某些资源的访问,以防止多个线程同时修改同一数据, 从而避免竞争条件。它确保在任何时刻,只有一个线程能够执行被锁住的代码块。
在 C# 中,lock
关键字依赖于对象的互斥量(mutex)来实现线程同步。这种机制确保在任何时 刻,只有一个线程可以进入被 lock
保护的代码块,从而避免了多个线程对共享资源的并发访 问。
(3)lock实现机制:
-
对象互斥量:
lock
关键字实际上是对一个对象进行加锁。这个对象通常是一个object
类型的实例。在lock
关键字内部,使用的是系统级别的互斥量来实现这一机制。这个互斥量确保同一时间只有一个线程可以访问被锁定的代码块。 -
Monitor 类:在后台,
lock
关键字利用了System.Threading.Monitor
类来实现锁定操作。Monitor
提供了一些方法,比如Enter
和Exit
,用于获取和释放锁。lock
关键字实际上是对这些方法的语法糖,使得代码更简洁和易读。
(4)lock工作原理:
- 尝试获取锁:当一个线程执行到
lock
语句时,它会尝试获取指定对象的锁。 - 进入临界区:如果锁当前没有被其他线程持有,那么线程会成功获取锁并进入被保护的代码块。
- 释放锁:当线程执行完锁保护的代码块后,它会释放锁,允许其他线程获取该锁。
(5)lock的使用:在需要锁住的共享资源(经常是一份公共可修改的变量数据)逻辑实现外用
lock(locker) //locker是引用类型的参数
{ } 结构来锁住这段逻辑,实现线程同步和线程安全
8.前后台程序
通过Thread创建的线程默认是前台的
前台线程:程序必须等待所有前台线程结束后才能退出
通过ThreadPool创建的线程默认是后台的
后台线程:程序不考虑后台线程,当程序结束时后台线程随之结束
Unity程序结束时,前台程序随即关闭,总结:在unity里其实不必过度区分前后台线程,有基础的认识即可,通过Thread内部成员字段 isBackground来描述
9.线程状态
我们先了解四种常用的线程状态:
(1)Unstarted:未启动状态,创建线程对象时默认的状态
(2)Running:运行状态,执行绑定的方法(注意:Running状态的线程其实还是走走停停的, 等待CPU分配时间片)
(3)WaitSleepJoin:等待睡眠阻塞状态,暂时停止执行,将资源交给其他线程使用
(4)Stoppd:终止状态,线程销毁
(5)AbortRequested:在 Unity 中,AbortRequest
是 UnityEngine.AsyncOperation
类中的一个线程状态标志。它表示操作请求被中止,即异步操作正在被取消或已被标记为中止。这通常在加载场景或资源时,操作被取消或需要中止时使用。
应用场景:
创建线程 线程为Unstarted状态
使用信号灯,System.Sleep() 等手段使阻止线程
线程执行结束,线程进入Stopped状态,随即释放
常在Unity生命周期函数 OnApplicationQuit()【此函数将在程序即将结束时调用】 内执行myThread.Abort(); 主要作用是请求终止一个正在执行的线程。
10.实现在线程内调用Unity内置API(线程缺点解决方案)
最后说一下unity里线程的缺点之三:辅助线程不能访问Unity API
我们进行一个例子的引入:我想在一个辅助线程里实现 Canvas上的一个Text组件打印数字倒计时的功能,
代码1版
using System.Threading;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
public class ThreadTest:MonoBehaviour
{
Thread myThread1;
public TMP_Text myText;
public float deadTime = 160;
private void Start()
{
myThread1=new Thread(()=>
{
while (deadTime > 0)
{
deadTime--;
myText.text = "Time:" + (int)(deadTime / 60) + ":" + (int)(deadTime % 60);
Thread.Sleep(1000);
}
});
myThread1.Start();
}
}
也许你已经经过尝试,启动!结果console窗口里却出现了一位不速之客:
报错显示:get_time只能在主线程中调用
这下怎么办呢,既然线程内部无法调用Unity的内置API,我们就避开线程内部调用,将函数内的逻辑交给一个委托(UnityAction来处理)
代码2版
using System.Threading;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
public class ThreadTest : MonoBehaviour
{
Thread myThread1;
public TMP_Text myText;
public float deadTime = 160;
UnityAction myAction;
private void Start()
{
myThread1 = new Thread(() =>
{
while (deadTime > 0)
{
deadTime--;
myAction = () => { myText.text = "Time:" +
(int)(deadTime / 60) + ":" +
(int)(deadTime % 60); };
Thread.Sleep(1000);
}
});
myThread1.Start();
}
private void Update()
{
myAction?.Invoke();
myAction = null;
}
}
编辑器内运行,发现功能已经可以正常实现
这就相当于将线程内要执行的逻辑放入一个委托中,Updata函数内时刻检测此委托内是否含有方法逻辑 ,有就执行
我们便可抽象出一个线程委托Manager来专门管理线程 并实现 线程内调用Uinty内置API的功能
见下方图所示
二.综合应用实现(线程管理及线程调用Unity内置APIManager)
由于是在学习过程中自行尝试实现,可能会有不足之处及改进空间 %(^-*)% 希望各位大佬不吝赐教
1.实现目标
在任意脚本中实现 自行控制开启时间(延迟)辅助线程的运行 并且 能正常执行Unity内置方法
(类似用辅助线程实现出协程的效果,但线程和协程这两者肯定是风马牛不相及的,达咩搞混~)
不妨就以上面举出的Text例子来尝试一下
2.实现思路
3.代码设计
(1)ThreadDelayAction:定义一种数据结构,包含委托和延迟时间,下称 委托复合单元
(2)RiseThread:定义一种数据结构,包含线程和其他高维变量, 下称 线程复合单元
(3)ThreadActionManager所需功能:
1.包含委托复合单元的列表
2.开启目标线程复合单元内的线程
3.Update内时刻遍历列表内是否有延迟时间满足执行的委托逻辑
(4)ThreadRiseManager所需功能:
1.包含线程复合单元的字典<string riseThreadName,RiseThread riseThread>
2.向线程复合单元内的线程植入委托
3.满足条件能够开启目标线程复合单元内的线程
4.实现源码
ThreadRiseManager类
using System.Collections.Generic;
using System.Threading;
using UnityEngine.Events;
public class RiseThread
{
public Thread thread;
//string threadName;
//public UnityAction threadAction;
/// <summary>
/// 未来可能加入多维信息
/// </summary>
/// <param name="_thread"></param>
public RiseThread(Thread _thread)
{
thread = _thread;
}
}
public class ThreadRiseManager : MonoSingleton<ThreadRiseManager>
{
Dictionary<string, RiseThread> threadDic = new Dictionary<string, RiseThread>();
RiseThread threadRise1;
RiseThread threadRise2;
RiseThread threadRise3;
RiseThread threadRise4;
RiseThread threadRise5;
RiseThread threadRise6;
List<RiseThread> threadRiseList = new List<RiseThread>();
Thread thread1 = new Thread(() => { });
Thread thread2 = new Thread(() => { });
Thread thread3 = new Thread(() => { });
Thread thread4 = new Thread(() => { });
Thread thread5 = new Thread(() => { });
Thread thread6 = new Thread(() => { });
public override void Init()
{
threadRise1 = new RiseThread(thread1);
threadRise2 = new RiseThread(thread2);
threadRise3 = new RiseThread(thread3);
threadRise4 = new RiseThread(thread4);
threadRise5 = new RiseThread(thread5);
threadRise6 = new RiseThread(thread6);
threadRiseList.Add(threadRise1);
threadRiseList.Add(threadRise2);
threadRiseList.Add(threadRise3);
threadRiseList.Add(threadRise4);
threadRiseList.Add(threadRise5);
threadRiseList.Add(threadRise6);
AddDicListener();
}
void AddDicListener()
{
for (int i = 1; i <= threadRiseList.Count; i++)
{
AddSingleDicListener("threadRise" + i, threadRiseList[i - 1]);
}
}
void AddSingleDicListener(string riseThreadName, RiseThread riseThread)
{
if (!threadDic.ContainsKey(riseThreadName))
{
threadDic.Add(riseThreadName, riseThread);
}
}
//外部直接传入函数方法,传入后将此函数传入对应threadRise的委托函数内,
//先在目标线程内添加待执行函数,等待时机开启线程
public void SetMyThread(string threadRiseName, UnityAction action)
{
if (threadDic.ContainsKey(threadRiseName))
{
if (threadDic[threadRiseName].thread != null)
{
print("目标threadRise已添加监听委托");
threadDic[threadRiseName].thread = new Thread(() => { action(); });
}
}
//如果没有发现目threadRise,则返回
else
{
print("错误:未发现目标threadRise!");
}
}
/// <summary>
/// 开启目标线程
/// </summary>
/// <param name="threadRiseName"></param>
/// <param name="excute"></param>
public void StartMyThread(string threadRiseName, bool excute)
{
if (threadDic.ContainsKey(threadRiseName))
{
if (!threadDic[threadRiseName].thread.IsAlive)
{
print("开始执行线程:" + threadRiseName);
if (!threadDic[threadRiseName].thread.IsAlive)
{
threadDic[threadRiseName].thread.Start();
}
}
}
}
/// <summary>
/// 程序结束,自动终结所有辅助线程
/// </summary>
private void OnApplicationQuit()
{
foreach (var item in threadDic)
{
if (item.Value.thread != null)
item.Value.thread.Abort();
}
}
}
ThreadActionManager类
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class ThreadDelayAction
{
public UnityAction threadAction;
public float timeDelay;
public ThreadDelayAction(UnityAction _threadAction, float _timeDelay)
{
threadAction = _threadAction;
timeDelay = _timeDelay;
}
}
public class ThreadActionManager : MonoSingleton<ThreadActionManager>
{
private List<ThreadDelayAction> threadDelayActionLists=new List<ThreadDelayAction>();
object locker = new object();
protected override void Awake()
{
base.Awake();
}
private void Update()
{
for (int i = threadDelayActionLists.Count - 1; i >= 0; i--)
{
if (threadDelayActionLists[i].timeDelay <= Time.time)
{
threadDelayActionLists[i].threadAction?.Invoke();
threadDelayActionLists.Remove(threadDelayActionLists[i]);
}
}
}
/// <summary>
/// 实际执行开启的线程
/// </summary>
/// <param name="action"></param>
/// <param name="delay"></param>
public void AddThreadStartListener(UnityAction action, float delay=0)
{
//锁一下list,线程同步,更安全
lock (threadDelayActionLists)
{
ThreadDelayAction newThreadDelayAction = new ThreadDelayAction(action, delay);
threadDelayActionLists.Add(newThreadDelayAction);
print("新添加辅助线程委托监听");
}
}
}
ThreadHelpTest类(测试脚本)
using System.Threading;
using TMPro;
using UnityEngine;
public class ThreadHelpTest : MonoBehaviour
{
public TMP_Text myText;
public float deadTime = 160;
private void Start()
{
ThreadRiseManager.Instance.SetMyThread("threadRise1", ThreadFun1);
}
private void Update()
{
//自行控制线程的开启时间
if (Input.GetKey(KeyCode.Space))
{
print("按下了Space,开启待执行线程");
ThreadRiseManager.Instance.StartMyThread("threadRise1", true);
}
}
void ThreadFun1()
{
while (deadTime > 0)
{
deadTime--;
ThreadActionManager.Instance.AddThreadStartListener(() =>
{
myText.text = "Time:" + (int)(deadTime / 60) + ":" + (int)(deadTime % 60);
}, 5);
Thread.Sleep(1000);
}
}
}
需要注意的问题:
1.MonoSingleton脚本我直接使用了在专栏Unity小框架里的MonoSingleton脚本
2.在使用前先挂载ThreadRiseManager和ThreadActionManager脚本
由于所有的线程都需要提前创建,所以需要先把ThreadRiseManager和ThreadActionManager挂载场景内游戏物体上在Awake阶段对线程管理系统进行初始化,否则直接在外部添加线程委托的时候会有报空异常
本篇完