Unity多线程使用

<meta charset="utf-8">

一、什么是线程?

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

简单理解:

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

image.png

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

好接下来我们举例说明一下,QQ这个进程(软件),我们把进程和线程的关系当成脚本中我们写的类和函数的关系,要知道我们的类其实就是一个大功能(软件),类中的方法函数就是为了帮助实现这个类的某个功能(软件的某个功能)。他们之间的关系 就是 一个类中包含多个方法函数并且类中至少有一个方法(一个类中一个方法都没有,没有操作计算没有意义,这里举例参数类不算.....)。

二、为什么要使用线程?

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

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

三、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 定义的基本类型的函数可以在分线程运行

四、线程的生命周期

image.png

线程的生命周期包含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()已经过时,因为它们是不安全的,为什么呢?这涉及到CPU的调度问题,人们为了将CPU充分的利用起来,在数据处理时,CPU的调度是不确定,举个例子,我们在生活中给别人打电话的时候左手执行接听电话这个任务,这时候电话那头人说了一个号码,你需要用右手来记录,虽然在同一时间执行了这两个任务,但是当你用左手接听电话那边说的数字的时候,右手是不可能同时写的(除非你有预言功能,在他说之前就已经知道号码了),这时候的处理方式就是当他说出一部分号码时,右手趁着他喘口气的时间,在本子上记下来。CPU的处理方式也是如此。如果我们在左手接电话的任务中调用Suspend()方法,这时候右手任务趁虚而入,那么我们会发现我们想挂起的左手任务还在执行,不想挂起的任务却被暂停了,这样在数据处理中是一个很严重的事情。

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

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);
        }
    }

}

运行结果:

image.png

我们可以看出打印结果是无序的(虽然是交错打印的,实际他们的运行方式是,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("A线程: ");
    }

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

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

}

结果:

image.png

看结果就知道变成顺序打印了,但是要注意一点 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);
        }
    }
}

结果:

3d64e33a69ea4a338275eacc37bced05.gif

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

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();
    }

}

结果:

![892b3b6a7fa34048b8fbbab49c4c69b8.gif](https://upload-images.jianshu.io/upload_images/26067915-7cdcbafd6f4c9ec6.gif?imageMogr2/auto-orient/strip)

这里打印结果没啥作用,其实你们可以试一下,在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();
    }

}

结果:

892b3b6a7fa34048b8fbbab49c4c69b8.gif

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结束运行后,还是会执行线程方法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容