浅谈Lua协程和函数的尾调用

news2024/10/7 2:30:38

前言

虽然不经常用到协程,但是也不能谈虎色变。同时,在有些场景,协程会起到一种不可比拟的作用。所以,了解它,对于一些功能,也会有独特的思路和想法。

协程

概念

关于进程和线程的概念就不多说。

那么从多线程的角度来看,协程和线程有点类似:拥有自己的栈,局部变量和指令指针,又和其他协程共享全局变量等一切资源。

主要的区别在:一个多线程程序可以并行运行多个线程,而协程却要彼此协作运行。

什么是协作运行?

也就是任意指定的时刻,只能有一个协程运行。

很懵对不对?

层级调用和中断调用

所有的语言中,都存在层级调用,比如A调用了B,B在执行过程中又去调用C,C执行之后,返回到B,B执行完毕之后,返回到A,最后A执行完毕。

这个过程就像栈一样,先进后出,依次从栈顶执行。所以,它也叫调用栈。

层级调用的方式是通过栈来实现的。

中断调用,就是我在A函数中可以中断去调用B函数,函数B中可以中断去调用A。

比如

function A()
	print(1)
	print(2)
	print(3)
end

function B()
	print("a")
	print("b")
	print("c")
end

那么如果是中断调用,就可能输出: 1 a b 2 3

协程的优势

其实一句话总结。

拿着多线程百分之一的价钱,干着多线程的事情,还效率贼高。

这样的员工谁不喜欢?

这也是Go语言高并发的原因之一。

怎么理解协程?

左手画圆,右手画方,两个手同时操作,这个叫并行,线程就是干这个事情的。
左手画一笔,切换到右手画一笔,来回交替,最后完成,这叫并发,协程就是为了并发而生。

在这里插入图片描述

那么线程不能完成协程的事情?

举个例子,一个作业,可以交给两个人完成,这叫并行。创建两个线程就可以了。
那么其中一个人在做事情的时候,突然有人要插入一个比较紧急的事情,这个人就必须停下手中的事情,去处理那个紧急的事情。停下叫阻塞。线程本身是支持阻塞的。

但是这里就有一个问题,不管是创建线程还是切换线程,所带来的成本远远大于用阻塞的方式实现并发,要考虑两个线程之间的数据同步,加锁解锁,临界问题等等。而协程并不需要来回切换。所以,并发的线程越多,使用协程来代替的性能优势就越明显。

所以,可以知道协程不是线程,它的资源是共享的,不需要如同多线程一样加锁来避免读写冲突。

比如创建一个线程栈需要1M,那么协程栈只需要几K,或者几十K。

协程的缺点

协程本质上是单线程,所以它吃不到多核CPU的福利,需要与进程配合才能办到。

然后协程也不是那么好控制,需要写一些代码进行手动控制它的中断调用。

Lua的协程

Lua的协程是非对称协程。

简单来说,非对称就是需要两个函数来控制协程的执行。

Go的协程是对称协程,有兴趣可以去了解下。

再说简单一点就是,非对称协程,需要yield函数来挂起,resum函数来恢复,同时,哪里挂起,就恢复到哪里去。

对称协程,只需要yield一个操作。

简单的例子

理解完这个,就很容易理解Lua的协程代码。即在哪里yield,下次调用resum,就恢复到yield那里,继续执行。

local co = coroutine.create(function()
    print(1)
    coroutine.yield()
    print(2)
    coroutine.yield()
    print(3)
end)

coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)

第一次resume,就是执行协程函数co ,print(1)
第二次resume,就是print(2)
第三次resume,就是print(3)

四种状态

一个协程有四种状态:

  1. 挂起(suspended)
  2. 运行(running)
  3. 正常(normal)
  4. 死亡(dead)

可以通过函数**coroutine.status(co)**来进行检查协程的状态。

当一个协程创建的时候,它处于一个挂起状态,也就是说,协程被创建,不会自动运行,需要调用函数**coroutine.resume(co)**用来启动或者再启动一个协程的执行,将挂起状态改成运行状态。

当协程中执行到最后的时候,整个协程就停止了,变成了死亡状态。

协程报错

由于协程resume在保护模式下,所有错误都会返回给它,即哪怕协程处于死亡状态,调用coroutine.resume(co),也不会出现任何错误。

同时协程中的错误不容易被发现,所以需要使用xpcall来进行抛出。

lua5.1需要封装一层来达到这个目的

--协程,Lua5.1无法挂C函数,这样进行处理,协程中出问题,会抛出错误
local coroutCanTrowError = function()
    xpcall(showGetSpecialCollect,__G__TRACKBACK__)
end
self.m_showGetSpecialRune = coroutine.create(coroutCanTrowError)
coroutine.resume(self.m_showGetSpecialRune)

xpcall函数,异常处理函数,是lua强大的异常处理函数,这个以后做分享。

交换数据

lua的协程主要是通过resume-yield来进行数据交换的。

即第一个resume函数会把所有的额外参数传递给协程的主函数。

什么意思呢?

local co = coroutine.create(function(a,b,c)
		print("co",a,b,c)
	end)

coroutine.resume(co,1,2,3) 

resum函数会把参数都传递给协程的主函数。所以这里输出co 1 2 3

local co = coroutine.create(function(a,b,c)
	    print("co",a,b,c)
		coroutine.yield(a + b,a - b , c + 2)
	end)

print(coroutine.resume(co,1,2,3))

输出就是

co 1 2 3

true 3 -1 5

在yield的时候,会把参数都返回给resume,这里有点拗口。
可以这么理解,yield中的参数就是resum的返回值。当然这里要注意的是,resume的返回值第一个是resume是否成功的标志。

这种类似于

local co = coroutine.create(function(a,b,c)
		return a - b
	end)

print(coroutine.resume(co,1,2,3))

输出 true,-1

也就是协程主函数的返回值都会变成resume的返回值。

是不是觉得有无限可能了?

但是,值得注意的是,虽然这种机制会带来很大的灵活性,但是,使用不好,可能会导致代码的可读性降低。

著名的生产者和消费者

这是协程最经典的例子。

即一个生产函数(从文件读数据),一个消费函数(将读出来的值写入另一个文件)。

function producer()
	while true do 
		local x = 1
		send(x)
	end
end

function consumer()
	while true do 
		local x = receive()
		print(x)
    end
end

local list = {}
function send(x)
	table.insert(list,x)
end

function receive()
	if #list > 0 then 
	   local a = table.remove(list)
		return a
    end
end

producer()
consumer()

会发生什么?

当然,也可以将两个放到不同的线程中去处理,但是这样对于数据量大的时候来说就是个灾难。

协程怎么去实现?感兴趣的可以去了解下。

function eceive (prod) 
	local status, value = coroutine esume(prod)
	return value 
end 

function send (x) 
	cooutine.yield(x)
end 
function poducer()
	return coroutine.c eate(function () 
		while true do 
			local x = io.read () 
			send (x) 
		end 
	end) 
end 
function filter(prod)
	return coroutine.ceate(func () 
		for line= 1, math.huge do 
		local x = receive (prod) 
		x = string.format(%s ”, line, x)
		send(x)
		end 
		end ) 
end 

function conrumer(prod)
	while true do 
		local x = receivee(prod) 
		io. write (x ,”\n”) 
	end 
end 
conrumer(filter(poducer()))

不用担心性能问题,因为是协程,所以任务开销很小,基本上消费者和生产者是携手同行。

应用场景

首先,再重复一遍,协程是并发,不是并行。

有个需求,和我们相关的。

棋盘有4种bonus,停轮之后,每个bouns的效果不一样,比如翻转,比如收集等等,效果执行完毕之后,再进行下一个,直到结束。

这个是很简单的需求,可以用for循环执行。

如果加上,bonus在执行效果中,会有部分等待,或者延迟效果?那么for循环就不能满足,因为在延迟的时候,函数就返回,执行下一个for循环了。

再比如加上,bonus在执行效果中,会牵涉到另外一堆逻辑。

等等。

然后,有人会说,递归也可以实现。

但是,首先得明白一点,递归是个思想,而协程是个机制。两个本质上不是一个东西,更何况,递归会涉及到其他东西。这个等下会说。

递归

递归的含义是:在调用一个函数的过程中,直接或者间接调用了函数本身。

一个很简单的递归就是:

local a = nil
a = function(n)
    if n == 1 then 
        return 1
    end
    print("a "..n)
    n = n * a(n - 1)
    print("b "..n)
    return n
end
print(a(5))

请问输出什么。

输出

a 5
a 4
a 3
a 2
b 2
b 6
b 24
b 120
120

在这里插入图片描述

再来温习下递归的特点:

  1. 原来的基础上不断“向下/向内”开辟新的内存空间。(即每一次的调用都会增加一层栈,每当函数返回的时候,就减少一层栈。)所以,对于递归来说,递归的层次越多,很容易导致栈溢出。这也决定了递归本身的效率不高。
  2. 递归是暂停阻塞,什么是暂停阻塞呢?也就是递归调用函数的以下部分是不会被执行的,只有返回之后才能执行。

说到这里,不得不说lua这个语言有一个非常不错的优化——尾调用。

尾调用

什么是尾调用呢?

一个函数返回您一个函数的返回值。

是不是有点拗口。

我们看下代码。

function A(x)
	return B(x)
end

通俗的来说,就是当一个函数调用是另一个函数的最后一个动作的时候,该调用才能算上尾调用。

上面例子中,A调用完B之后,就没有任何逻辑了。这个时候,Lua程序不需要返回函数A所有在得函数,那么程序自然而然就不需要保存任何有关于函数A的栈(stack)信息。

该特性叫“尾调用消除”。

别小看这个特性。

通常,函数在调用的时候,会在内存中形成一个“调用记录”,它保存了调用位置和内部变量等信息。

例如

function A(x)
  local a = 1
	 local b = B(x)
	 local c = 2
  return a + b + c
end

在程序运行到调用函数B的时候,会在A的调用记录上方,形成B的调用记录,等到B返回之后,B的调用记录才会消失。那么调用的函数越多,就如同栈一样,依次放到A、B等等的上面,所有的调用记录就形成了调用栈。

那么想象一下,函数A调用B,B调用C,C调用D…会发生什么。

栈溢出

栈和堆不一样,栈是系统分配的,也可以说栈是系统预设的空间,堆是自己申请的。所以,当我们的函数调用层次太深,导致保存调用记录的空间大于了系统分配的栈,那么就会导致栈溢出。然后各种莫名其妙的Bug就出现了,比如递归不返回了;比如调用函数不返回了等等。

这个时候,Lua的尾调用消除就起到了关键性作用。它是函数最后一步操作,所以不需要保存外层函数的调用记录,它里面的所有内部变量等信息都不会再用到了,所以就不需要栈空间去保存这些信息。

那么,可能会有人说,那么我在函数尾部调用另一个函数不就可以了么?

并不是。

function A(x)
	return 1 + B(x)
end

function C(x)
	return true and D(x)
end

上面的两个函数就不是尾调用。

函数A中,最后一步不是B函数,而是+运算符

函数C中,最后一步不是D,而是and 运算符

function A(x)
	if x > 0 then 
		return B(x)
	end
	return C(x)
end

这样的函数B和函数C才是尾调用。

可能这样觉得没啥,我们做个实验。

function A(x)
    return 1 + B(x)
end 

function B(x)
    return x * 2
end

print(collectgarbage("count"))
for i = 1,1000000 do 
--print(A(i))
	A(i)
end
print(collectgarbage("count"))

输出
在这里插入图片描述
相差:0.09960

那么看下尾调用

function A(x)
	return B(x,1)
end

function B(x,y)
	return x * 2 + y
end

print(collectgarbage("count"))
for i = 1,1000000 do 
--print(A(i))
	A(i)
end
print(collectgarbage("count"))

在这里插入图片描述
相差是0.035

接近3倍。

可能有人会说,这点性能应该没啥吧。我想说的是,这里只是一个简单的计算,本来保存的数据都不大,如果是实际开发中,需要保存的东西更大了。

尾递归

理解了尾调用,那么我们来看看尾递归

前面也说了,递归依赖栈,非常消耗内存。还有,别以为有些功能就递归几次,就没有什么性能消耗。那谁又能保证在递归的函数中有大量的其他函数调用或者数据处理呢?

毕竟有时候很容易写出,递归函数中调用其他函数,其他函数又调用一堆其他函数这种套娃的代码。

例如,最开始的代码

local a = nil
a = function(n)
    if n == 1 then 
        return 1
    end
    return n * a(n - 1)
end
a(5)

这就是一个“不合格”的递归函数。

那么尾递归怎么写呢?

local a = nil
a = function(n,m)
    if n == 1 then 
        return m
    end
    return a(n - 1,n * m)
end

a(5,1)

回调

通常会拿协程同回调也就是callback比较。因为两者都可以实现异步通信。

比如:

bob.walkto(jane)
bob.say("hello")
jane.say("hello")

当然,不可能这样运行,那么会导致一起出现。所以有下面的方式。

bob.walto(function (  )
	bob.say(function (  )
		jane.say("hello")
	end,"hello")
end, jane)

如果再多一些呢?

再结合上面说的调用记录的说法,可能层次深了,发现咋不回调了。

如果用协程来实现就是:

function runAsyncFunc( func, ... )
	local current = coroutine.running
	func(function (  )
		coroutine.resume(current)
	end, ...)
	coroutine.yield()
end
 
coroutine.create(function (  )
	runAsyncFunc(bob.walkto, jane)
	runAsyncFunc(bob.say, "hello")
	jane.say("hello")
end)
 
coroutine.resume(co)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/939541.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

这所985专业课均分130!复试笔试很难!淘汰率很高!

一、学校及专业介绍 东南大学坐落于六朝古都南京,是享誉海内外的著名高等学府。学校是国家教育部直属并与江苏省共建的全国重点大学,是国家“双一流”、“985工程”、“211工程”重点建设高校。2017年,东南大学入选世界一流大学建设名单。 …

字符集(Latin1,GBK,utf8,utf8mb4)

Latin1 1个字符占一个字节GBK 1个字符占两个字节utf8utfmb3 1个字节占三个字节utf8mb4 1个字符占四个字节

OLED透明屏高清:什么是OLED透明屏?未来显示技术的巅峰之作

在现代科技快速发展的时代,高清显示已经成为人们对于视觉体验的基本要求。 而OLED透明屏作为一种先进的显示技术,以其出色的高清显示效果和透明度,正逐渐成为未来显示技术的巅峰之作。 一、什么是OLED透明屏 OLED透明屏采用有机发光二极管技…

Zabbix下载安装及SNMP Get使用

帮助文档:6. Zabbix Appliance 一、zabbix下载安装 1、获取Zabbix Appliance镜像 Download Zabbix appliance 2、使用该镜像创建虚拟机 3、打开虚拟机控制台自动安装,等待安装完成即可 默认配置 系统/数据库:root:zabbix Zabbix 前端&am…

javacv基础01-HelloWorld

JavaCV是一个针对Java编程语言的开源计算机视觉和机器学习库。它为各种流行的计算机视觉和图像处理库(如OpenCV、FFmpeg等)提供了Java包装,使Java开发人员能够在其Java应用程序中访问这些库的功能,无需编写本地代码。 JavaCV提供…

SpringBoot Mybatis 多数据源 MySQL+Oracle

一、背景 在SpringBoot Mybatis 项目中&#xff0c;需要连接 多个数据源&#xff0c;连接多个数据库&#xff0c;需要连接一个MySQL数据库和一个Oracle数据库 二、依赖 pom.xml <dependencies><dependency><groupId>org.springframework.boot</groupId&…

(Deep Learning)准确率和召回率的基础概念

算法模型极大的提升了对各类结果的预测效率。 【算法模型的本质】 算法模型的本质&#xff0c;是基于输入的各类变量因子&#xff0c;通过计算规则&#xff08;模型or公式&#xff09;&#xff0c;得出预测结果。 典型的预测结果比如&#xff1a; 1.&#xff08;通过历史行为…

天地图开发指南

1、 申请天地图key 1.1注册账号 注册地址&#xff1a;https://uums.tianditu.gov.cn/register 1.2 申请开发者 登录后 &#xff0c;申请开发者https://console.tianditu.gov.cn/api/register 1.3 创建应用 点击控制台&#xff0c;创建应用 1.4 天地图key 2、天地图api使用 2.…

如何开发一款飞机聊天app?即时通讯系统

随着航空业的快速发展&#xff0c;飞机旅行已经成为人们生活中常见的一部分。而在飞行期间&#xff0c;人们往往希望能够与其他乘客进行交流&#xff0c;分享旅行经历或者寻找旅途中的伴侣。为了满足这一需求&#xff0c;开发一款专门用于飞机上的聊天应用程序成为了一个有意的…

个人博客系统——SSM框架

项目特点&#xff1a; 1.使用手工加盐算法代替明文&#xff0c;提高用户隐私安全性 2.登录功能的验证使用了拦截器 3.支持分布式 Session存储和缓存都放到了Redis里面 具体实现步骤 1.创建一个SSM项目 ​​​​​​​ 2.准备项目 先删除项目中无用的文件和目录 引入前端…

项目进度管理:项目经理做了无用功,如何解决?

李思是一个职场新人&#xff0c;项目经理分配了一个简单的任务给她&#xff0c;完成一份关于竞品功能的调查报告&#xff0c;以便为公司的产品提供参考。 李思第二天回复称进展顺利&#xff0c;预计两天时间能完成。 然而&#xff0c;当项目经理收到厚厚的调研报告时&#x…

Gitlab创建一个空项目

1. 创建项目 Project slug是访问地址的后缀&#xff0c;跟前边的ProjectUrl拼在一起&#xff0c;就是此项目的首页地址&#xff1b; Visibility Level选择默认私有即可&#xff0c;选择内部或者公开&#xff0c;就会暴露代码。 勾选Readme选项&#xff0c;这样项目内默认会带…

怎么看待目前的游戏市场格局,看好哪儿家公司?

近三十年来&#xff0c;中国游戏砥砺前行经过近四十年的发展&#xff0c;将计算机技术、互动媒体技术、艺术设计、经济系统、商业模式等进行了充分融合应用&#xff0c;作为 “ 第九艺术 ” 已经成为文化产业 的重要支柱&#xff0c;其硬件和软件创新也不断改变着人们的娱乐消费…

小研究 - 多租户Java虚拟机的设计与实现(二)

多租户技术&#xff0c;让一个软件实例同时服务于不同的组织&#xff0c;在云计算环境中被广泛运用&#xff0c;极大的节约了基础设施资源。但是&#xff0c;云计算环境中使用最广的Java语言却没有提供相应的多租户功能。为此&#xff0c;云服务提供商不得不对自己的应用服务器…

arcgis的MapServer服务查询出来的结果geometry坐标点带*的问题

不知道小伙伴使用arcgis server服务做查询的时候&#xff0c;有没有遇到下面的问题 原因是查询结果中出现*字符 这个问题一直困扰了我很久&#xff1a;因为从数据库查询的坐标点是没有问题的。 一开始有同事遇到过&#xff0c;说重新插入下就好了&#xff0c;有时候确实能解决…

Qt-creater 在线安装太慢,换国内源

Qt 在线安装太慢,换国内源 下载安装包 实例使用清华源 如下图先下载安装包exe文件 url: 链接: https://mirrors.tuna.tsinghua.edu.cn/qt/official_releases/online_installers/ 下载安装包到本地目录D:\ Powershell进入本地目录D:\ 使用参数方式换国内清华源 换国内清华源 …

嵌入式AI助力当代商业的发展

数字化转型的业务影响是广泛的&#xff0c;但购买者应寻求嵌入式AI在以下领域具有最大的影响力&#xff1a; 1.业务流程和任务的自动化 当买家搜索购买包含AI的软件时&#xff0c;他们应该研究该解决方案为员工自动执行日常任务的方式。嵌入式AI应该节省员工的时间和精力&#…

Maven之高版本的 lombok 和 tomcat 7 插件冲突问题

高版本的 lombok 和 tomcat 7 插件冲突问题 在开发期间&#xff0c;当我们使用 tomcat7-maven-plugin 来作为运行环境运行我们项目使&#xff0c;如果我们项目中使用了 1.16.20 及以上版本的 lombok 包&#xff0c;项目启动时会报错&#xff1a; for annotations org.apache.…

工业级PDA高精度导航定位

工业级PDA是指能到达防尘、防水、防摔三防等级&#xff0c;并具备实时采集、自动存储、即时显示、即时反馈、自动处理和自动传输等功能的移动智能终端。为满足如农业、铁路、空间、勘测与绘图等复杂环境领域的需要&#xff0c;目前高端工业级PDA普遍具备高精度的导航定位功能&a…

Apple Configurator iphone ipad 设备管控 描述文件使用方法

一、准备 App Store 下载安装 Apple Configurator 二、Apple Configurator 注册组织&#xff0c; -----------这个组织可以是个人&#xff0c;或者其它组织导出-------再导入进来&#xff1a; 三、描述文件配置&#xff1a;“” 根据管控需求进行配置 “” 四、使用 Ap…