在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)。

/// 
/// Template class to make a job using a thread. 
/// 
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;
    }
    /// 
    /// Is set to true when the job is finished
    /// 
    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;
    /// 
    /// 核心逻辑区
    /// 
    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的打印次数,那么效果很明显。

                                                                   在unity中,自己动手实现多线程作业功能----使用C#的线程_第1张图片

在一帧之内打印三十次,一次性完成。

                                                                 在unity中,自己动手实现多线程作业功能----使用C#的线程_第2张图片

打印10000*3次,耗时 (191 +5)帧* Time.deltaTime的时间。

                                                                 在unity中,自己动手实现多线程作业功能----使用C#的线程_第3张图片

三个线程输出是没有顺序的。

在学习的路上,不会一帆风顺,若有任何错误,欢迎指正。感谢。希望能解决您的问题。

你可能感兴趣的:(游戏开发,Unity多线程作业)