Unity多线程知识点记录

一、什么是线程?

线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

简单理解: 

我们首先了解一下什么是进程。我们电脑开启的每个软件其实就是一个进程。Ctrl+alt+delete 选择任务管理器可以查看

 

为什么要先了解进程呢?因为进程和线程是包含关系,一个进程(软件)中是包含多个线程的。并且一个进程至少要有一个线程。

二、为什么要使用线程?

线程其实是同时(并行)执行的,在Unity中虽然有协程可以协助主线程进行计算,但是协程的计算还是在主线程中的,如果协程要计算的数据过大,需要等待,这时候就会影响主线程的其他方法执行,比如我们在UpDate中实现鼠标控制相机旋转移动,这时候协程计算某个数据等待了2秒,你就明显发现屏幕卡顿了(因为主线程在计算东西,相机旋转在后面等着呢)。

所以这时候就用到线程了。有了线程 我不管你计算的数据多么庞大,我主线程根本不怕(两者各干各的 互不影响)。

三、Unity可以使用多线程,但却要避免使用线程

在 Unity 中,你不能直接使用传统的线程同步和并发机制,因为 Unity 是基于单线程的执行模型的。Unity 的主要执行线程是主线程(也称为渲染线程),它负责处理游戏逻辑、渲染和用户输入等。直接在 Unity 中创建和操作线程是不安全的,会导致不可预测的结果,可能会破坏 Unity 的执行流程,甚至导致程序崩溃。 

Unity自己本身UnityEngine所使用的API是不能被多线程调用的,所以Unity是不能使用多线程的,但是C#中可以使用多线程,Unity使用C#进行脚本编辑,故而Unity也可以通过C#来调用多线程。

Unity使用多线程时要注意几点:

  1. 变量都是共享的(都能指向相同的内存地址)
  2. UnityEngine 的 API 不能在分线程运行
  3. UnityEngine 定义的基本结构(int, float, struct 定义的数据类型)可以在分线程计算,如 Vector3(struct)可以, 但 Texture2d(class,根父类为 Object) 不可以。
  4. UnityEngine 定义的基本类型的函数可以在分线程运行

详细信息: 

Unity之可以使用多线程,但为何要避免使用。_被代码折磨的狗子的博客-CSDN博客

四、线程的生命周期

线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。

  • 新建:就是刚使用new方法,new出来的线程;

  • 就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;

  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;

  • 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;

  • 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;

五、线程基础方法使用 

new Thread():创建一个线程

start():开启创建的线程

join():当前线程等待另一个线程结束后,在执行

Sleep();等待N毫秒后继续执行

Suspend():该方法并不终止未完成的线程,它仅仅挂起当前线程,以后还可恢复;

Resume():恢复被Suspend()方法挂起的线程的执行。

Abort():结束线程

 Suspend(挂起)、Resume(恢复)已经过时,其中Suspend(挂起)方法被认为是不安全的,因为它可能会导致线程死锁、死活锁或其他不可预测的行为。举例一个发生死锁的案例,如果一个A线程调用Txt文本资源用于数据处理,这个文本资源就会上“锁”,然后我们使用Suspend方法将A线程进行挂起操作,此时被A线程调用的文本资源并未得到释放。这时B线程也需要调用Txt文本资源进行数据处理。又因为A线程没有释放文本资源导致发生死锁。

PS:死锁是多线程或多进程并发编程中的一种问题,指的是两个或多个线程或进程互相持有对方所需要的资源,导致它们都无法继续执行下去的一种状态。这种状态下,每个线程或进程都在等待对方释放资源,而自己又不释放已经持有的资源,从而导致所有参与的线程或进程都无法继续执行,形成了死锁

接下来教大家上面方法如何使用

1.线程调用有参无参的方法函数

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;

public class ThreadTest : MonoBehaviour
{
    Thread threadA;
    Thread threadB;
    void Start()
    {
        threadA = new Thread(AA);
        threadB = new Thread(new ParameterizedThreadStart(BB));
        threadA.Start();
        threadB.Start("B线程: ");
    }

    //无参 
    void AA()
    {
        for (int i = 0; i < 5; i++)
        {
            Debug.Log("A线程: " + i);
        }
    }

    //有参 注意有参函数类型必须是object类型
    void BB(object a) 
    {
        for (int i = 0; i < 5; i++)
        {
            Debug.Log(a.ToString() + i);
        }
    }

}

运行结果:

 我们可以看出打印结果是无序的(虽然是交错打印的,实际他们的运行方式是,A线程在打印的时候 CPU有空余时间,这时候B线程直接顶上,这么做能充分的利用CPU)

2.Join()方法使用 

我们接下来让A线程循环打印完毕后在执行B线程循环,这就用到我们的Join方法了

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;

public class ThreadTest : MonoBehaviour
{
    Thread threadA;
    Thread threadB;
    void Start()
    {
        threadA = new Thread(AA);
        threadB = new Thread(new ParameterizedThreadStart(BB));
        threadA.Start();
        threadA.Join();//在CPU加入threadA的结束判断当threadA线程结束后 在执行后面的线程方法
        threadB.Start("B线程: ");
    }

    //无参
    void AA()
    {
        for (int i = 0; i < 5; i++)
        {
            Debug.Log("A线程: " + i);
        }
    }

    //有参
    void BB(object a)
    {
        for (int i = 0; i < 5; i++)
        {
            Debug.Log(a.ToString() + i);
        }
    }

    private void OnApplicationQuit()
    {
        Debug.Log(123);
        if (threadA!=null)
        {
            threadA.Abort();
        }

        if (threadB != null)
        {
            threadB.Abort();
        }
    }
}

结果:

 看结果就知道变成顺序打印了,但是要注意一点 Join这个方法会占用很多CPU资源,要小心利用,Join在线程执行完之前分配大量的时间片给该线程,直到线程结束后。所以使用的时候要注意

3.Sleep()等待睡眠结束

这个很简单 就是执行到该语句时等待一段时间继续往下执行

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;

public class ThreadTest : MonoBehaviour
{
    Thread threadA;
    Thread threadB;
    void Start()
    {
        threadA = new Thread(AA);
        threadB = new Thread(new ParameterizedThreadStart(BB));
        threadA.Start();
        threadB.Start("B线程: ");
    }

    //无参
    void AA()
    {
        Debug.Log("A线程开启");
        Thread.Sleep(3000); //3000毫秒 等待3秒钟
        Debug.Log("A线程等待了3秒");

    }

    //有参
    void BB(object a) 
    {
        for (int i = 0; i < 3; i++)
        {
            Thread.Sleep(1000); //1000毫秒 等待1秒钟
            Debug.Log(a.ToString() + i);
        }
    }
}

结果:

 结果很直观吧 就不多解释了。

4.Abort() 停止线程

其实也叫"杀死"线程,执行这个方法后线程就被摧毁了,从线程生命周期来看他是自动执行的,这里举一个使用他的例子

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;

public class ThreadTest : MonoBehaviour
{
    Thread threadA;
    void Start()
    {
        threadA = new Thread(AA);
        threadA.Start();
    }

    //无参
    void AA()
    {
        //死循环 每过1秒执行一次
        while (true)
        {
            Thread.Sleep(1000); //3000毫秒 等待3秒钟
            Debug.Log("A线程执行");
        }
    }

    
    void OnApplicationQuit()
    {
        //结束线程必须关闭 否则下次开启会出现错误 (如果出现的话 只能重启unity了)
        threadA.Abort();
    }

}

结果:

 这里打印结果没啥作用,其实你们可以试一下,在OnApplicationQuit()不加threadA.Abort()方法的时候,Unity结束运行时你会发现还是会有打印效果。

5.做一个按钮控制开关线程

using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
using System.Threading;

public class ThreadTest : MonoBehaviour
{
    [SerializeField] Button btn;
    Thread threadA;
    void Start()
    {
        threadA = new Thread(AA);
        threadA.Start();

        btn.onClick.AddListener(delegate {
            btn.transform.GetChild(0).GetComponent<Text>().text = isStart ? "开启" : "暂停";
            isStart = !isStart; 
        });
    }

    bool isStart = false;
    //无参
    void AA()
    {
        //死循环 每过1秒执行一次
        while (true)
        {
            if (isStart)
            {
                Debug.Log("A线程执行");
                Thread.Sleep(1000); //1000毫秒 等待1秒钟
            }
        }
    }
    
    void OnApplicationQuit()
    {
        //结束线程必须关闭 否则下次开启会出现错误 (如果出现的话 只能重启unity了)
        threadA.Abort();
    }

}

结果: 

 6.使用协程控制线程一秒打印一次

using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
using System.Threading;
using System.IO;

public class ThreadTest : MonoBehaviour
{
    Thread threadA;
    void Start()
    {
        threadA = new Thread(AA);
        threadA.Start();
        StartCoroutine(Test());
    }

    IEnumerator Test()
    {
        while (true)
        {
            isEnd = true;
            yield return new WaitForSeconds(1f);
        }
    }

    bool isEnd = false;
    //无参
    void AA()
    {
        while (true)
        {
            if (isEnd)
            {
                isEnd = false;
                Debug.Log("A线程执行");
            }
        }

    }

    void OnApplicationQuit()
    {
        //结束线程必须关闭 否则下次开启会出现错误 (如果出现的话 只能重启unity了)
        threadA.Abort();
    }

}

六、线程池的使用

 .NET Framework的ThreadPool类提供一个线程池,该线程池可用于执行任务、发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器。那么什么是线程池?线程池其实就是一个存放线程对象的“池子(pool)”,他提供了一些基本方法,如:设置pool中最小/最大线程数量、把要执行的方法排入队列等等。ThreadPool是一个静态类,因此可以直接使用,不用创建对象。

有点类似Unity中的对象池,当要使用线程的时候我们线程池查找是否有空闲的线程,有就使用,没有就创建生成。

微软官网说法如下:

许多应用程序创建大量处于睡眠状态,等待事件发生的线程。还有许多线程可能会进入休眠状态,这些线程只是为了定期唤醒以轮询更改或更新的状态信息。 线程池,使您可以通过由系统管理的工作线程池来更有效地使用线程。 

所以线程池一般是在需要大量线程,并且线程的数据处理都很小的情况下使用

使用方法很简单:

using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
using System.Threading;
using System.IO;

public class ThreadTest : MonoBehaviour
{
    void Start()
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback(AA), null);
    }

    //无参
    void AA(object a)
    {
        Debug.Log("A线程执行");
    }

}

这里要注意几点:

1.线程池的方法必须是有参方法,而且传参不能超过2个

2.如果方法使用死循环,Unity结束运行后,还是会执行线程方法。

  • 9
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值