Unity网络编程(1)多线程

引入:网络编程基础认识

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 提供了一些方法,比如 EnterExit,用于获取和释放锁。lock 关键字实际上是对这些方法的语法糖,使得代码更简洁和易读。

(4)lock工作原理:

  1. 尝试获取锁:当一个线程执行到 lock 语句时,它会尝试获取指定对象的锁。
  2. 进入临界区:如果锁当前没有被其他线程持有,那么线程会成功获取锁并进入被保护的代码块。
  3. 释放锁:当线程执行完锁保护的代码块后,它会释放锁,允许其他线程获取该锁。

(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 中,AbortRequestUnityEngine.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阶段对线程管理系统进行初始化,否则直接在外部添加线程委托的时候会有报空异常

本篇完

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值