在unity中,自己動手實現多線程作業功能----使用C#的線程

疫情當前,呆着家裏,總結關於工作中遇到的問題和解決思路,是一個很好的學習機會,珍愛生命,懂得感恩,一切會好起來的。

一、寫在之前

之前的一篇文章中https://blog.csdn.net/THUNDERDREAMER_OR/article/details/104213670,我提到了網格規劃算法的計算量比較大,對三個切面分別進行計算的時候,使用多線程是一個不錯的想法,實際上我也是這麼做的。現在我們來分析一下,怎樣使用C#的Threading自己動手實現多線程並行計算。

知識預熱。涉及到多線程,不得不說多因線程之間的通信和競爭而導產生問題,因此鎖機制和“死鎖”的概念便應用而生。

  1. 鎖機制

在競爭態條件下,多個線程對同一競態資源的搶奪會引發線程安全問題。競態資源是對多個線程可見的共享資源,主要包括全局(非const)變量、靜態(局部)變量、堆變量、資源文件等。

線程之間的競爭,可能帶來一系列問題:

  1. 線程在操作某個共享資源的過程中被其他線程打斷,時間片耗盡而被迫切換到其他線程
  2. 共享資源被其中一個線程修改之後,並未通知同步到其他線程,造成線程間的數據訪問不一致
  3. 編譯器優化等導致若干操作指令的執行順序打亂而造成的結果

爲了解決由多進程多資源的同時操作引起的問題,提出了鎖機制。在某一時刻只允許有一個進程運行它的臨界區代碼,從而保證臨界資源的中數據的一致性。臨界資源是指能夠被多個線程共享的數據、資源。臨界區代碼是指多臨界資源操作的那段代碼。

這裏只介紹互斥鎖(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的時間。

                                                                 

三個線程輸出是沒有順序的。

在學習的路上,不會一帆風順,若有任何錯誤,歡迎指正。感謝。希望能解決您的問題。

發佈了6 篇原創文章 · 獲贊 1 · 訪問量 391
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章