转载请标明出处:http://www.cnblogs.com/zblade/

一、序言

在unity的游戏开发中,对于异步操作,有一个避免不了的操作: 协程,以前一直理解的懵懵懂懂,最近认真充电了一下,通过前辈的文章大体理解了一下,在这儿抛砖引玉写一些个人理解。当然首先给出几篇写的非常精彩优秀的文章,最好认真拜读一下:

王迅:Coroutine从入门到劝退​zhuanlan.zhihu.com

Unity3d中协程的原理,你要的yield return new xxx的真正理解之道​blog.csdn.net

Unity协程(Coroutine)原理深入剖析​dsqiu.iteye.com

好了,接下来就从一个小白的视角开始理解协程。

二、常见使用协程的示例

经常,我们会利用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.

unity协程coroutine浅析的更多相关文章

  1. Unity协程(Coroutine)管理类——TaskManager工具分享

    博客分类: Unity3D插件学习,工具分享 源码分析   Unity协程(Coroutine)管理类——TaskManager工具分享 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处 ...

  2. Unity协程Coroutine使用总结和一些坑

    原文摘自 Unity协程Coroutine使用总结和一些坑 MonoBehavior关于协程提供了下面几个接口: 可以使用函数或者函数名字符串来启动一个协程,同时可以用函数,函数名字符串,和Corou ...

  3. Unity 协程(Coroutine)原理与用法详解

    前言: 协程在Unity中是一个很重要的概念,我们知道,在使用Unity进行游戏开发时,一般(注意是一般)不考虑多线程,那么如何处理一些在主任务之外的需求呢,Unity给我们提供了协程这种方式 为啥在 ...

  4. Unity 协程Coroutine综合测试

    using UnityEngine; using System.Collections; using System.Text; public class rotCube : MonoBehaviour ...

  5. Unity协程(Coroutine)原理深入剖析

    Unity协程(Coroutine)原理深入剖析 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 其实协程并没有那么复杂,网上很多地方都说是多 ...

  6. Unity协程(Coroutine)原理深入剖析(转载)

    记得去年6月份刚开始实习的时候,当时要我写网络层的结构,用到了协程,当时有点懵,完全不知道Unity协程的执行机制是怎么样的,只是知道函数的返回值是IEnumerator类型,函数中使用yield r ...

  7. Unity协程(Coroutine)原理深入剖析再续

    Unity协程(Coroutine)原理深入剖析再续 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 前面已经介绍过对协程(Coroutine ...

  8. 【转】Unity协程(Coroutine)原理深入剖析

    Unity协程(Coroutine)原理深入剖析 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 记得去年6月份刚开始实习的时候,当时要我写网 ...

  9. Unity怎样在Editor下运行协程(coroutine)

    在处理Unity5新的AssetBundle的时候,我有一个需求,须要在Editor下(比方一个menuitem的处理函数中,游戏没有执行.也没有MonoBehaviour)载入AssetBundle ...

随机推荐

  1. Go语言Context(设计及分析)

    context简单概述: Go服务器的每个请求都有自己的goroutine,而有的请求为了提高性能,会经常启动额外的goroutine处理请求,当该请求被取消或超时,该请求上的所有goroutines ...

  2. BZOJ_3573_[Hnoi2014]米特运输_树形DP+hash

    BZOJ_3573_[Hnoi2014]米特运输_树形DP+hash 题意: 给你一棵树每个点有一个权值,要求修改最少的权值,使得每个节点的权值等于其儿子的权值和且儿子的权值都相等. 分析: 首先我们 ...

  3. jenkins+ant+jmeter测试环境部署

    1.安装java 2.安装jenkins 3.下载apache-jmeter-4.0,解压后放在 /home/用户名/ 下 4.下载apache-ant-1.10.3,解压后放在 /home/用户名/ ...

  4. k8s编排最佳实践

    编排文件技巧 使用资源时指定最新稳定版的API version 编排文件应该纳入版本控制,这样可以在必要的时候快速回滚,同样也有利于资源恢复和重建 使用YAML格式而不是JSON格式,尽管两种格式的文 ...

  5. 我和Python的Py交易》》》》》》 浮点数的身世字谜

    什么是浮点数? 在数据类型中写道,浮点数是带小数点的小数,这个概念是不准确的:浮点数是除了无限不循环小数之外的小数,也就是可以用分数表示的带小数点的数. 好了,浮点数就这些内容,讲完了,各回各家,各找 ...

  6. Resnet论文翻译

    摘要 越深层次的神经网络越难以训练.我们提供了一个残差学习框架,以减轻对网络的训练,这些网络的深度比以前的要大得多.我们明确地将这些层重新规划为通过参考输入层x,学习残差函数,来代替没有参考的学习函数 ...

  7. 深入理解Java 栈数据结构

    栈(stack)又名堆栈,它是一种运算受限的线性表.其限制是仅允许在表的一端进行插入和删除运算.这一端被称为栈顶,相对地,把另一端称为栈底.向一个栈插入新元素又称作进栈.入栈或压栈,它是把新元素放到栈 ...

  8. 一文学会Scala

    整体介绍 Scala 是一门多范式(multi-paradigm)的编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性. 联邦理工学院洛桑(EPFL)的Martin Odersky于2001 ...

  9. 从壹开始微服务 [ DDD ] 之十二 ║ 核心篇【下】:事件驱动EDA 详解

    缘起 哈喽大家好,又是周二了,时间很快,我的第二个系列DDD领域驱动设计讲解已经接近尾声了,除了今天的时间驱动EDA(也有可能是两篇),然后就是下一篇的事件回溯,就剩下最后的权限验证了,然后就完结了, ...

  10. Java异常处理最佳实践及陷阱防范

    前言 不管在我们的工作还是生活中,总会出现各种“错误”,各种突发的“异常”.无论我们做了多少准备,多少测试,这些异常总会在某个时间点出现,如果处理不当或是不及时,往往还会导致其他新的问题出现.所以我们 ...