疫情當前,呆着家裏,總結關於工作中遇到的問題和解決思路,是一個很好的學習機會,珍愛生命,懂得感恩,一切會好起來的。
一、寫在之前
之前的一篇文章中https://blog.csdn.net/THUNDERDREAMER_OR/article/details/104213670,我提到了網格規劃算法的計算量比較大,對三個切面分別進行計算的時候,使用多線程是一個不錯的想法,實際上我也是這麼做的。現在我們來分析一下,怎樣使用C#的Threading自己動手實現多線程並行計算。
知識預熱。涉及到多線程,不得不說多因線程之間的通信和競爭而導產生問題,因此鎖機制和“死鎖”的概念便應用而生。
- 鎖機制
在競爭態條件下,多個線程對同一競態資源的搶奪會引發線程安全問題。競態資源是對多個線程可見的共享資源,主要包括全局(非const)變量、靜態(局部)變量、堆變量、資源文件等。
線程之間的競爭,可能帶來一系列問題:
- 線程在操作某個共享資源的過程中被其他線程打斷,時間片耗盡而被迫切換到其他線程
- 共享資源被其中一個線程修改之後,並未通知同步到其他線程,造成線程間的數據訪問不一致
- 編譯器優化等導致若干操作指令的執行順序打亂而造成的結果
爲了解決由多進程多資源的同時操作引起的問題,提出了鎖機制。在某一時刻只允許有一個進程運行它的臨界區代碼,從而保證臨界資源的中數據的一致性。臨界資源是指能夠被多個線程共享的數據、資源。臨界區代碼是指多臨界資源操作的那段代碼。
這裏只介紹互斥鎖(mutex lock)的概念和原理,另外一種是自旋鎖。這裏的互斥,指的就是不同線程之間的互斥性、排他性,即當一個線程在使用臨界資源的時候,不允許其他線程對該資源操作。
互斥鎖是一種很常見應用也很廣的鎖,屬於sleep-waiting類型,即在鎖處於佔用狀態時,其他線程自動掛起等待,直到該鎖釋放,線程再次競爭。鎖的掛起和釋放的切換會消耗一定的性能。
本質上,互斥鎖是一個變量(mutex),在使用它時,實際上是對mutex的置0置1操作,mutex狀態的改變使線程能獲得鎖和釋放鎖。
2. 死鎖
維基百科關於死鎖(Deadlock)的解釋:In concurrent computing, a deadlock is a state in which each member of a group is waiting for another member, including itself, to take action, such as sending a message or more commonly releasing a lock. Deadlock is a common problem in multiprocessing systems, parallel computing, and distributed systems, where software and hardware locks are used to arbitrate shared resources and implement process synchronization.通俗來講,假設線程A持有鎖a,線程B持有鎖b,而線程訪問臨界區的條件是,同時具有鎖a和鎖b,那麼線程A等待線程B釋放鎖b,線程B等待線程A釋放鎖a,如果沒有外部的作用,線程A、B會一直等待下去,從而產生死鎖。
二、代碼實現
分析:C#專門提供有線程的類,在System.Threading的命名空間下。在本例中,我們的線程父類需要具備以下的特點:初始化線程並啓動線程的方法(Start),標識線程作業是否執行完成的字段(isDone)。
/// <summary>
/// Template class to make a job using a thread.
/// </summary>
public class ThreadedJob
{
private bool m_IsDone = false;
private object m_Handle = new object();
private System.Threading.Thread m_Thread = null;
private void Run()
{
ThreadFunction();
IsDone = true;
}
/// <summary>
/// Is set to true when the job is finished
/// </summary>
public bool IsDone
{
get
{
bool tmp;
lock (m_Handle)
{
tmp = m_IsDone;
}
return tmp;
}
set
{
lock (m_Handle)
{
m_IsDone = value;
}
}
}
public virtual void Start()
{
m_Thread = new System.Threading.Thread(Run);
m_Thread.Start();
}
public virtual void Abort()
{
if(m_Thread != null)
m_Thread.Abort();
}
public virtual void Interrupt(){
if (m_Thread != null)
m_Thread.Interrupt ();
}
public virtual bool Update()
{
if (IsDone)
{
OnFinished();
return true;
}
return false;
}
protected virtual void ThreadFunction() { }
protected virtual void OnFinished() { }
}
C#中 lock 語句就是互斥鎖的實現,如果主線程(或其他子線程同時)訪問/修改 IsDone 的變量(臨界資源),lock 獲取 m_Thread 的互斥鎖,如果 m_Thread 處於鎖住狀態,掛起等待,否則,m_Thread 處於釋放狀態,鎖住該對象,並執行 lock 塊中的語句,然後釋放 m_Thread。釋放 m_Thread 之後,變量 IsDone 被訪問/修改,如果有修改,修改結果會同步到其他線程,這樣,其他線程在訪問 IsDone 變量的時候,是更新過的數據。
using UnityEngine;
public class ThreadJobExecutor : ThreadedJob
{
public ThreadJobExecutor (int threadId)
{
this.ThreadId = threadId;
}
public int ThreadId;
/// <summary>
/// 核心邏輯區
/// </summary>
protected override void ThreadFunction()
{
try
{
// here is your heavy job to execute, for example
for (int i = 0; i < 10; i++)
{
Debug.Log("in thread " + ThreadId + " debug info!");
}
}
catch (System.Exception e)
{
Debug.Log("the thread interrupted exeption: " + e.Message);
// clear logic
}
finally
{
}
}
}
下面測試代碼:
using System.Collections;
using UnityEngine;
public class Game : MonoBehaviour
{
ThreadJobExecutor[] exectors = new ThreadJobExecutor[3];
private IEnumerator Start()
{
for (int i = 0; i < exectors.Length; i++)
{
exectors[i] = new ThreadJobExecutor(i + 1);
}
for (int i = 0; i < exectors.Length; i++)
{
exectors[i].Start();
}
do
{
Debug.Log("finished job count is: " + finishCount());
yield return null;
}
while (finishCount()!= exectors.Length);
Debug.Log("all jogs hava finished");
}
int finishCount()
{
int cnt = 0;
for (int i = 0; i < exectors.Length; i++)
{
if (exectors[i].IsDone)
cnt++;
}
return cnt;
}
}
測試打印0-9十個數字,不需要等待即可完成,如果是0-99999的打印次數,那麼效果很明顯。
在一幀之內打印三十次,一次性完成。
打印10000*3次,耗時 (191 +5)幀* Time.deltaTime的時間。
三個線程輸出是沒有順序的。
在學習的路上,不會一帆風順,若有任何錯誤,歡迎指正。感謝。希望能解決您的問題。