<meta charset="utf-8">
一聘鳞、什么是線程?
線程是操作系統(tǒng)能夠進行運算調度的最小單位锹雏,被包含在進程之中巴比,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流礁遵,一個進程中可以并發(fā)多個線程轻绞,每條線程并行執(zhí)行不同的任務。
簡單理解:
我們首先了解一下什么是進程佣耐。我們電腦開啟的每個軟件其實就是一個進程政勃。Ctrl+alt+delete 選擇任務管理器可以查看
為什么要先了解進程呢?因為進程和線程是包含關系兼砖,一個進程(軟件)中是包含多個線程的奸远。并且一個進程至少要有一個線程。
好接下來我們舉例說明一下讽挟,QQ這個進程(軟件)懒叛,我們把進程和線程的關系當成腳本中我們寫的類和函數的關系,要知道我們的類其實就是一個大功能(軟件)耽梅,類中的方法函數就是為了幫助實現這個類的某個功能(軟件的某個功能)薛窥。他們之間的關系 就是 一個類中包含多個方法函數并且類中至少有一個方法(一個類中一個方法都沒有,沒有操作計算沒有意義眼姐,這里舉例參數類不算.....)诅迷。
二、為什么要使用線程众旗?
線程其實是同時(并行)執(zhí)行的竟贯,要知道在Unity中雖然有協程可以協助主線程進行計算,但是協程的計算還是在主線程中的逝钥,如果協程要計算的數據過大屑那,需要等待拱镐,這時候就會影響主線程的其他方法執(zhí)行,比如我們在UpDate中實現鼠標控制相機旋轉移動持际,這時候協程計算某個數據等待了2秒沃琅,你就明顯發(fā)現屏幕卡頓了(因為主線程在計算東西,相機旋轉在后面等著呢)蜘欲。
所以這時候就用到線程了益眉。有了線程 我不管你計算的數據多么龐大,我主線程根本不怕(兩者各干各的 互不影響)姥份。
三郭脂、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 定義的基本類型的函數可以在分線程運行
四忍弛、線程的生命周期
線程的生命周期包含5個階段,包括:新建考抄、就緒细疚、運行、阻塞川梅、銷毀惠昔。
- 新建:就是剛使用new方法,new出來的線程挑势;
- 就緒:就是調用的線程的start()方法后镇防,這時候線程處于等待CPU分配資源階段,誰先搶的CPU資源潮饱,誰開始執(zhí)行;
- 運行:當就緒的線程被調度并獲得CPU資源時来氧,便進入運行狀態(tài),run方法定義了線程的操作和功能;
- 阻塞:在運行狀態(tài)的時候香拉,可能因為某些原因導致運行狀態(tài)的線程變成了阻塞狀態(tài)啦扬,比如sleep()、wait()之后線程就處于了阻塞狀態(tài)凫碌,這個時候需要其他機制將處于阻塞狀態(tài)的線程喚醒扑毡,比如調用notify或者notifyAll()方法。喚醒的線程不會立刻執(zhí)行run方法盛险,它們要再次等待CPU分配資源進入運行狀態(tài);
- 銷毀:如果線程正常執(zhí)行完畢后或線程被提前強制性的終止或出現異常導致結束瞄摊,那么線程就要被銷毀勋又,釋放資源;
五、線程基礎方法使用
new Thread():創(chuàng)建一個線程
start():開啟創(chuàng)建的線程
join():當前線程等待另一個線程結束后换帜,在執(zhí)行
Sleep();等待N毫秒后繼續(xù)執(zhí)行
Suspend():該方法并不終止未完成的線程楔壤,它僅僅掛起當前線程,以后還可恢復惯驼;
Resume():恢復被Suspend()方法掛起的線程的執(zhí)行蹲嚣。
Abort():結束線程
其中 Suspend(),Resume()已經過時祟牲,因為它們是不安全的隙畜,為什么呢?這涉及到CPU的調度問題说贝,人們?yōu)榱藢PU充分的利用起來议惰,在數據處理時,CPU的調度是不確定狂丝,舉個例子换淆,我們在生活中給別人打電話的時候左手執(zhí)行接聽電話這個任務哗总,這時候電話那頭人說了一個號碼几颜,你需要用右手來記錄,雖然在同一時間執(zhí)行了這兩個任務讯屈,但是當你用左手接聽電話那邊說的數字的時候蛋哭,右手是不可能同時寫的(除非你有預言功能,在他說之前就已經知道號碼了)涮母,這時候的處理方式就是當他說出一部分號碼時谆趾,右手趁著他喘口氣的時間,在本子上記下來叛本。CPU的處理方式也是如此沪蓬。如果我們在左手接電話的任務中調用Suspend()方法,這時候右手任務趁虛而入来候,那么我們會發(fā)現我們想掛起的左手任務還在執(zhí)行跷叉,不想掛起的任務卻被暫停了,這樣在數據處理中是一個很嚴重的事情营搅。
接下來教大家上面方法如何使用
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線程循環(huán)打印完畢后在執(zhí)行B線程循環(huán),這就用到我們的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線程結束后 在執(zhí)行后面的線程方法
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);
}
}
}
結果:
看結果就知道變成順序打印了休蟹,但是要注意一點 Join這個方法會占用很多CPU資源沸枯,要小心利用日矫,Join在線程執(zhí)行完之前分配大量的時間片給該線程,直到線程結束后辉饱。所以使用的時候要注意
3.Sleep()等待睡眠結束
這個很簡單 就是執(zhí)行到該語句時等待一段時間繼續(xù)往下執(zhí)行
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() 停止線程
其實也叫"殺死"線程,執(zhí)行這個方法后線程就被摧毀了彭沼,從線程生命周期來看他是自動執(zhí)行的缔逛,這里舉一個使用他的例子
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()
{
//死循環(huán) 每過1秒執(zhí)行一次
while (true)
{
Thread.Sleep(1000); //3000毫秒 等待3秒鐘
Debug.Log("A線程執(zhí)行");
}
}
void OnApplicationQuit()
{
//結束線程必須關閉 否則下次開啟會出現錯誤 (如果出現的話 只能重啟unity了)
threadA.Abort();
}
}
結果:
這里打印結果沒啥作用,其實你們可以試一下姓惑,在OnApplicationQuit()不加threadA.Abort()方法的時候褐奴,Unity結束運行時你會發(fā)現還是會有打印效果。
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()
{
//死循環(huán) 每過1秒執(zhí)行一次
while (true)
{
if (isStart)
{
Debug.Log("A線程執(zhí)行");
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線程執(zhí)行");
}
}
}
void OnApplicationQuit()
{
//結束線程必須關閉 否則下次開啟會出現錯誤 (如果出現的話 只能重啟unity了)
threadA.Abort();
}
}
六于毙、線程池的使用
.NET Framework的ThreadPool類提供一個線程池敦冬,該線程池可用于執(zhí)行任務、發(fā)送工作項唯沮、處理異步 I/O脖旱、代表其他線程等待以及處理計時器。那么什么是線程池介蛉?線程池其實就是一個存放線程對象的“池子(pool)”萌庆,他提供了一些基本方法,如:設置pool中最小/最大線程數量币旧、把要執(zhí)行的方法排入隊列等等践险。ThreadPool是一個靜態(tài)類,因此可以直接使用吹菱,不用創(chuàng)建對象巍虫。
有點類似Unity中的對象池,當要使用線程的時候我們線程池查找是否有空閑的線程鳍刷,有就使用占遥,沒有就創(chuàng)建生成。
微軟官網說法如下:
許多應用程序創(chuàng)建大量處于睡眠狀態(tài)输瓜,等待事件發(fā)生的線程瓦胎。還有許多線程可能會進入休眠狀態(tài),這些線程只是為了定期喚醒以輪詢更改或更新的狀態(tài)信息前痘。 線程池凛捏,使您可以通過由系統(tǒng)管理的工作線程池來更有效地使用線程。
所以線程池一般是在需要大量線程芹缔,并且線程的數據處理都很小的情況下使用
使用方法很簡單:
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線程執(zhí)行");
}
}
這里要注意幾點:
1.線程池的方法必須是有參方法坯癣,而且傳參不能超過2個
2.如果方法使用死循環(huán),Unity結束運行后最欠,還是會執(zhí)行線程方法示罗。