Derivation of MonoBehaviour coroutine coroutine and its use in Lua

Derivative use of unity coroutine asynchronous operation and its use in Lua

1. Examples of commonly used coroutines

Often, we use monobehaviour's startcoroutine to start a coroutine, which is the most common intuitive understanding we use in unity. Perform some asynchronous operations in this coroutine, such as downloading files, loading files, etc. After completing these operations, execute our callback. for example:

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;
      }
}

In this example, several keywords are used: IEnumerator/yield return xxx/ yield break/StartCoroutine, then we start with these keywords to understand the specific implementation of such a download operation.

1. Keyword IEnumerator

This keyword is not unique to Unity, and unity comes from c#, so it is more appropriate to find an example of c# to understand. First look at the definition of IEnumerator:

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

It can be understood from the definition that an iterator has three basic operations: Current/MoveNext/Reset. Here is a brief description of its operation process. In common collections, when we use enumeration operations such as foreach, at the beginning, the enumerator is set to be in front of the first element of the collection, and the Reset operation is to return the enumerator to this position.

When the iterator executes the iteration, it will first execute a MoveNext. If it returns true, it means that there is an object at the next position, and then set Current to the next object at this time. At this time, Current points to the next object. Of course, how c# compiles this IEnumrator into an object example for execution, which will be explained below.

2. Keyword Yield

There are two basic expressions behind the yield keyword in c#:

Yield return
yiled break
Yield break is the operation of jumping out of the coroutine, which is generally used in places where an error is reported or the coroutine needs to be exited.

Yield return is a more frequently used expression. The specific expresion can be the following common examples:

WWW: Common web operations, called at the end of each frame, will check isDone/isError, if true, call MoveNext
WaitForSeconds: check whether the interval is up, return true, then call MoveNext
null: directly call MoveNext
WaitForEndOfFrame: call after rendering , Call MoveNext

2. Compilation result of c# call to coroutine

Here, instead of compiling and generating the above example, just borrow the example from the previous article: b

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

C++ results generated by its compiler:

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; 
            } 
        } 
    } 
}

The code is more intuitive, and the relevant comments are also written a little, so when we execute a coroutine, the essence is to return an instance of iterator, and then in the main thread, every update will update this instance. Determine whether to perform the MoveNext operation, if it can be performed (such as the file download is complete), perform MoveNext once, and assign the next object to Current (MoveNext needs to return true, if it is false, it means the iteration is completed).

From here, we can get a conclusion that the coroutine is not asynchronous, its essence is still executed in the main thread of Unity, and each update will trigger whether to execute MoveNext.

Third, the derivative use of coroutines

Since IEnumerator can be used in this way, we can actually write a simple test coroutine example using only MoveNext and Current.

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;
        }
    }
}

Let me talk about the idea: At the beginning, build an IEnuerator instance and stuff it into the linked list, and then in each subsequent frame update, take out this instance, execute MoveNext once, and remove this instance after all executions are completed. Calling StartCoroutine without displaying it can also trigger the execution of MoveNext similarly

Look at the results:
Insert picture description here

feasible. OK, the coroutine about unity is written here, and then I will look at the realization of the coroutine in xlua.

Four, the coroutine in Lua

The difference between the coroutine in Lua and the unity coroutine is that it is not preemptive execution, which means that operations like MoveNext will not be actively executed, but we need to actively stimulate execution, just like the previous example The same, tick the operation yourself.

The three key APIs of the coroutine in Lua:

coroutine.create()/wrap: build a coroutine, wrap build result is function, create is thread type object

coroutine.resume(): Perform an operation similar to MoveNext

coroutine.yield(): Suspend the coroutine

Relatively simple, you can write an example to test it:

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)
--此时会输出 112/ 212/ 312/412/512
 
co1 = coroutine.create(func1)
coroutine.resume(co1, 1, 2)
--此时会输出 112 然后挂起
coroutine.resume(co1, 3, 4)
--此时将上次挂起的协程恢复执行一次,输出: 2, 1, 2 所以新传入的参数34是无效的

Let’s take a look at how to use the coroutine in the util open sourced by xlua and how to combine it with the coroutine of lua. Build the coroutine on the lua side, so that the c# side can also get this instance and add it to the main thread of the unity side. To trigger update.

Take a look at the API called:

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
}

The essence of start operation is to wrap the function one layer, call util.csgenerator, and take a closer look at the implementation of cs_generator in util:

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

The code is short, but the idea is very clear. First, build a table, where the key corresponds to a function, and then modify the _index method to remove the meta table, which includes the implementation of the MoveNext function and the implementation of the Reset function, but here is Reset is different from IEnumerator, here is to call coroutine.wrap to generate a coroutine. In this way, after the c# side obtains the handleID of this generator, MoveNext will be executed every subsequent frame update. If all are executed, move_end will be returned at this time, indicating that the coroutine has been executed. Return false to the c# side to clear the handleID of the coroutine .

Guess you like

Origin blog.csdn.net/qq_43505432/article/details/109744685