unity 协同程序

好久不见. 提交于 2020-08-10 08:14:21

转自: https://www.cnblogs.com/zblade/p/9857808.html

一、什么是协同程序?

在主线程运行的同时开启另一段逻辑处理,来协助当前程序的执行,协程很像多线程,但是不是多线程,Unity的协程实在每帧结束之后去检测yield的条件是否满足。

协同程序和线程差不多,也就是一条执行序列,拥有自己独立的栈、局部变量和指针, 同时又与其他协同程序共享全局变量和其他大部分东西。

与线程区别:一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作地运行。
就是说一个具有多个协同程序的程序在任意时刻只能和运行一个协同程序,并且正在运行的协同程序只会在显示地要求挂起(suspend)时,它的执行才会暂停。

一个协同程序可以处于四种不同的状态:
  • 挂起(suspended)
  • 运行(running)
  • 死亡(dead)
  • 正常(normal)

https://www.cnblogs.com/zblade/p/9857808.html

二、常见使用协程的示例

 

经常,我们会利用monobehaviour的startcoroutine来开启一个协程,这是我们在使用unity中最常见的直观理解。在这个协程中执行一些异步操作,比如下载文件,加载文件等,在完成这些操作后,执行我们的回调。 举例说明:

 

public   static   void   Download(System.Action finishCB)
{
       string   url =  "https: xxxx" ;
       StartCoroutine(DownloadFile(url));
}
 
private   static   IEnumerator DownloadFile( string   url)
{
      UnityWebRequest request = UnityWebRequest.Get(url);
      request.timeout = 10;
      yield   return   request.SendWebRequest();
      if (request.error !=  null )     
      {
                 Debug.LogErrorFormat( "加载出错: {0}, url is: {1}" , request.error, url);
                 request.Dispose();
                 yield   break ;
       }
      
       if (request.isDone)
       {
             string   path =  "xxxxx" ;
             File.WriteAllBytes(path, request.downloadHandler.data);
             request.Dispose();
             yiled  break ;
       }
}
 
这个例子中,用到了几个关键词: IEnumerator/yield return xxx/ yield break/StartCoroutine, 那么我们从这几个关键词入手,去理解这样的一个下载操作具体实现。
 

1、关键词 IEnumerator

这个关键词不是在Unity中特有,unity也是来自c#,所以找一个c#的例子来理解比较合适。首先看看IEnumerator的定义:

public   interface   IEnumerator
{
      bool   MoveNext();
      void   Reset();
      Object Current{ get ;}
}

从定义可以理解,一个迭代器,三个基本的操作:Current/MoveNext/Reset, 这儿简单说一下其操作的过程。在常见的集合中,我们使用foreach这样的枚举操作的时候,最开始,枚举数被定为在集合的第一个元素前面,Reset操作就是将枚举数返回到此位置。

迭代器在执行迭代的时候,首先会执行一个 MoveNext, 如果返回true,说明下一个位置有对象,然后此时将Current设置为下一个对象,这时候的Current就指向了下一个对象。当然c#是如何将这个IEnumrator编译成一个对象示例来执行,下面会讲解到。

2、关键词 Yield

c#中的yield关键词,后面有两种基本的表达式:

yield return <expresion>
yiled break

yield break就是跳出协程的操作,一般用在报错或者需要退出协程的地方。

yield return是用的比较多的表达式,具体的expresion可以以下几个常见的示例:

WWW : 常见的web操作,在每帧末调用,会检查isDone/isError,如果true,则 call MoveNext
WaitForSeconds: 检测间隔时间是否到了,返回true, 则call MoveNext
null: 直接 call MoveNext
WaitForEndOfFrame: 在渲染之后调用, call MoveNext

好了,有了对几个关键词的理解,接下来我们看看c#编译器是如何把我们写的协程调用编译生成的。

三、c#对协程调用的编译结果

这儿没有把上面的例子编译生成,就借用一下前面文章中的例子 :b

class   Test
{
      static   IEnumerator GetCounter()
      {
            for ( int   count = 0; count < 10; count++)
            {
                 yiled  return   count;
            }
       }
}

其编译器生成的c++结果:

internal  class   Test 
     // GetCounter获得结果就是返回一个实例对象
     private   static   IEnumerator GetCounter() 
    
         return   new   <GetCounter>d__0(0); 
    
   
     // Nested type automatically created by the compiler to implement the iterator 
     [CompilerGenerated] 
     private   sealed  class   <GetCounter>d__0 : IEnumerator<object>, IEnumerator, IDisposable 
    
         // Fields: there'll always be a "state" and "current", but the "count" 
         // comes from the local variable in our iterator block. 
         private   int   <>1__state; 
         private   object <>2__current; 
         public   int   <count>5__1; 
       
         [DebuggerHidden] 
         public   <GetCounter>d__0( int   <>1__state) 
        
            //初始状态设置
             this .<>1__state = <>1__state; 
        
   
         // Almost all of the real work happens here 
         //类似于一个状态机,通过这个状态的切换,可以将整个迭代器执行过程中的堆栈等环境信息共享和保存
         private   bool   MoveNext() 
        
             switch   ( this .<>1__state) 
            
                 case   0: 
                     this .<>1__state = -1; 
                     this .<count>5__1 = 0; 
                     while   ( this .<count>5__1 < 10)         //这里针对循环处理 
                    
                         this .<>2__current =  this .<count>5__1; 
                         this .<>1__state = 1; 
                         return   true
                     Label_004B: 
                         this .<>1__state = -1; 
                         this .<count>5__1++; 
                    
                     break
   
                 case   1: 
                     goto   Label_004B; 
            
             return   false
        
   
         [DebuggerHidden] 
         void   IEnumerator.Reset() 
        
             throw   new   NotSupportedException(); 
        
   
         void   IDisposable.Dispose() 
        
        
   
         object IEnumerator<object>.Current 
        
             [DebuggerHidden] 
             get 
            
                 return   this .<>2__current; 
            
        
   
         object IEnumerator.Current 
        
             [DebuggerHidden] 
             get 
            
                 return   this .<>2__current; 
            
        
    
}

 

代码比较直观,相关的注释也写了一点,所以我们在执行开启一个协程的时候,其本质就是返回一个迭代器的实例,然后在主线程中,每次update的时候,都会更新这个实例,判断其是否执行MoveNext的操作,如果可以执行(比如文件下载完成),则执行一次MoveNext,将下一个对象赋值给Current(MoveNext需要返回为true, 如果为false表明迭代执行完成了)。

通过这儿,可以得到一个结论,协程并不是异步的,其本质还是在Unity的主线程中执行,每次update的时候都会触发是否执行MoveNext。

四、协程的衍生使用

既然IEnumerator可以这样用,那我们其实可以只使用MoveNext和Current,就可以写一个简易的测试协程的例子,Ok,来写一个简易的例子,来自leader的代码,偷懒就复用了 :D

using   System.Collections;
using   System.Collections.Generic;
using   UnityEngine;
using   UnityEngine.Profiling;
 
public   class   QuotaCoroutine : MonoBehaviour
{
     // 每帧的额度时间,全局共享
     static   float   frameQuotaSec = 0.001f;
 
     static   LinkedList<IEnumerator> s_tasks =  new   LinkedList<IEnumerator>();
 
     // Use this for initialization
     void   Start()
     {
         StartQuotaCoroutine(Task(1, 100));
     }
 
     // Update is called once per frame
     void   Update()
     {
         ScheduleTask();
     }
 
     void   StartQuotaCoroutine(IEnumerator task)
     {
         s_tasks.AddLast(task);
     }
 
     static   void   ScheduleTask()
     {
         float   timeStart = Time.realtimeSinceStartup;
         while   (s_tasks.Count > 0)
         {
             var   t = s_tasks.First.Value;
             bool   taskFinish =  false ;
             while   (Time.realtimeSinceStartup - timeStart < frameQuotaSec)
             {
                 // 执行任务的一步, 后续没步骤就是任务完成
                 Profiler.BeginSample( string .Format( "QuotaTaskStep, f:{0}" , Time.frameCount));
                 taskFinish = !t.MoveNext();
                 Profiler.EndSample();
 
                 if   (taskFinish)
                 {
                     s_tasks.RemoveFirst();
                     break ;
                 }
             }
 
             // 任务没结束执行到这里就是没时间额度了
             if   (!taskFinish)
                 return ;
         }
     }
 
     IEnumerator Task( int   taskId,  int   stepCount)
     {
         int   i = 0;
         while   (i < stepCount)
         {
             Debug.LogFormat( "{0}.{1}, frame:{2}" , taskId, i, Time.frameCount);
             i++;
             yield   return   null ;
         }
     }
}

说一下思路: 在开始的时候,构建一个IEnuerator实例塞入链表中,然后再后续的每帧update的时候,取出这个实例,执行一次MoveNext,一直到都执行完后,移除这个实例,这样就不用显示的调用StartCoroutine,也可以类似的触发执行MoveNext :D

看运行结果:

可行。OK,关于unity的协程就写到这儿了,接下来将一下xlua中对于协程的实现。

五、Lua中的协程

Lua中的协程和unity协程的区别,最大的就是其不是抢占式的执行,也就是说不会被主动执行类似MoveNext这样的操作,而是需要我们去主动激发执行,就像上一个例子一样,自己去tick这样的操作。

Lua中协程关键的三个API:

coroutine.create()/wrap: 构建一个协程, wrap构建结果为函数,create为thread类型对象

coroutine.resume(): 执行一次类似MoveNext的操作

coroutine.yield(): 将协程挂起

比较简易,可以写也给例子测试一下:

local func = function(a, b)
     for   i= 1, 5  do
         print(i, a, b)
     end
end
 
local func1 = function(a, b)
     for   i = 1, 5  do
         print(i, a, b)
         coroutine.yield()
     end
end
 
 
co =  coroutine.create(func)
coroutine.resume(co, 1, 2)
--此时会输出 1 ,1, 2/ 2,1,2/ 3, 1,2/4,1,2/5,1,2
 
co1 = coroutine.create(func1)
coroutine.resume(co1, 1, 2)
--此时会输出 1, 1,2 然后挂起
coroutine.resume(co1, 3, 4)
--此时将上次挂起的协程恢复执行一次,输出: 2, 1, 2 所以新传入的参数3,4是无效的

我们来看看xlua开源出来的util中对协程的使用示例又是怎么结合lua的协程,在lua端构建也给协程,让c#端也可以获取这个实例,从而添加到unity端的主线程中去触发update。

看一下调用的API:

local util = require 'xlua.util'

local gameobject = CS.UnityEngine.GameObject('Coroutine_Runner')
CS.UnityEngine.Object.DontDestroyOnLoad(gameobject)
local cs_coroutine_runner = gameobject:AddComponent(typeof(CS.Coroutine_Runner))

return {
    start = function(...)
        return cs_coroutine_runner:StartCoroutine(util.cs_generator(...))
    end;

    stop = function(coroutine)
        cs_coroutine_runner:StopCoroutine(coroutine)
    end
}

start操作,本质就是将function包一层,调用util.csgenerator,进一步看看util中对cs_generator的实现

local move_end = {}

local generator_mt = {
    __index = {
        MoveNext = function(self)
            self.Current = self.co()
            if self.Current == move_end then
                self.Current = nil
                return false
            else
                return true
            end
        end;
        Reset = function(self)
            self.co = coroutine.wrap(self.w_func)
        end
    }
}

local function cs_generator(func, ...)
    local params = {...}
    local generator = setmetatable({
        w_func = function()
            func(unpack(params))
            return move_end
        end
    }, generator_mt)
    generator:Reset()
    return generator
end

代码很短,不过思路很清晰,首先构建一个table, 其中的key对应一个function,然后修改去元表的_index方法,其中包含了MoveNext函数的实现,也包含了Reset函数的实现,不过这儿的Reset和IEnumerator的不一样,这儿是调用coroutine.wrap来生成一个协程。这样c#端获取到这个generator的handleID后,后面每帧update回来都会执行一次MoveNext,如果都执行完了,这时候会return move_end,表明协程都执行完了,返回false给c#端清空该协程的handleID.

 

 

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!