Lua协同程序coroutine详解

在lua语言中有协同程序这一特性。协同程序是什么呢?

线程大家都知道吧,一系列指令组成一个线程,线程是程序执行流的最小单元,许多线程会组成一个进程,一般来说一个应用程序就是一个进程。协同程序和线程差不多,也是处于线程这一级别的执行流最小单元,并且都是由一系列代码指令组成。但协同程序和线程的最大区别就是:程序中的多个线程可以同时运行(即多线程),而多个协同程序却不能同时运行。

这里多说一句,多线程所谓的“同时”是指同一时间而不是同一时刻,CPU实际上在某一时刻只能执行某一条指令,所以多线程其实是在极短时间内多个线程不同切换以获得CPU使用权,因此多线程是人的一种宏观感受,从微观角度看,某一时刻其实还是只有一个线程的某一条指令在执行的。 


好了,下面我们就来说说这个协同程序到底是个啥:

Lua将所有关于协同程序的函数放置在一个名为“coroutine”的table中。

1.函数create()用于创建协同程序,它只有一个参数,就是一个函数。该函数的代码就是协同程序所需执行的内容create()会返回一个thread类型的值,用以表示该协同程序。通常create()的参数是一个匿名函数。

举个栗子:

local co = coroutine.create(function() print("我是个协同程序!") end)
print(co)
运行结果:

Lua协同程序coroutine详解_第1张图片

咦?协同程序里面的字都没有大出来,为什么打印出来这一串奇怪的东西是什么?别急,打印出来的这个就是我们create()的返回值,这是一个thread类型的值,可以理解为代表的就是我们创建的这个协同程序的ID。那我们协同程序里面的东西为什么没有被打印出来呢?因为它压根就没有被执行。这是为什么呢?

2.这里需要知道一点,一个协同程序可以处于4种不同的状态:挂起(suspended)、运行(running)、死亡(dead)和正常(normal)。当创建一个协同程序时,它处于挂起状态。也就是说,协同程序不会在创建它时自动执行其内容。我们可以通过status()方法来检查协同程序的状态

print(coroutine.status(co))

运行结果:

 Lua协同程序coroutine详解_第2张图片

看到了没,此时刚刚创建好的协同程序状态为suspended挂起状态。


那么怎么能让我们的协同程序运行起来呢?

3.函数coroutine.resume()用于启动或再次启动一个协同程序的执行,并将其状态由挂起改为运行态

coroutine.resume(co)
print(coroutine.status(co))

运行结果:

 Lua协同程序coroutine详解_第3张图片

这回协同程序执行了吧!但它的状态打印出来为什么是dead呢?因为此时协同程序已经执行完终止了,然后它就处于死亡状态了。


4.到目前为止,协同程序看上去还只是像一种复杂的函数调用方法。其实协同程序的真正强大之处在于函数yield()的使用上,该函数可以让一个运行中的协同程序挂起,而之后可以再恢复它的运行。举个栗子:

local co = coroutine.create(function() 
    for i = 1 , 10 do
        print("我也是个协同程序!  "  .. i)
        coroutine.yield()
    end
end)

我们先创建一个协同程序,大家可以看到,这个协同程序里有个for循环,每一次循环我们都打印一次(学过lua的应该都知道“..”在lua中代表字符串连接的意思),每次打印之后执行yield()方法。

当唤醒这个协同程序时,它就会开始执行,直到第一个yield:

coroutine.resume(co)  

运行结果:

 Lua协同程序coroutine详解_第4张图片

如果此时检查其状态,会发现协同程序处于挂起状态,因此可以再次恢复其运行:

print(coroutine.status(co))

运行结果:

 Lua协同程序coroutine详解_第5张图片

从协同程序的角度看,所有在它挂起时发生的活动都发生在yield()中(好自恋的协同程序)。当恢复协同程序的执行时,对于yield()调用才最终返回。然后协同程序继续它的执行,直到下一个yield调用或执行结束。

coroutine.resume(co) --第1次执行
coroutine.resume(co) --第2次执行
coroutine.resume(co) --第3次执行
coroutine.resume(co) --第4次执行
coroutine.resume(co) --第5次执行
coroutine.resume(co) --第6次执行
coroutine.resume(co) --第7次执行
coroutine.resume(co) --第8次执行
coroutine.resume(co) --第9次执行
coroutine.resume(co) --第10次执行
coroutine.resume(co) --第11次执行

运行结果:

 Lua协同程序coroutine详解_第6张图片

我们一个执行11次resume(),打印出来10次,最后一次什么都不打印,因为在最后一次调用resume()时,协同程序的内容已经执行完毕,并已经返回。因此,这时协同程序处于死亡状态。如果试图再次恢复它的执行,resume将返回false和一条错误消息:

print(coroutine.resume(co))

运行结果:

 

5.我们还需注意一下,resume()是在保护模式中运行的。因此,如果在一个协同程序的执行中发生任何错误,Lua是不会显示错误消息的,而是将执行权返回给resume()调用,我觉得这一点是协同程序的一大弊端。举个栗子:我们把刚刚协同程序里面的for循环修改一下然它产生错误:

for i = 1 , 10 do
    i = nil 
    print("我是for循环  "  .. i)
end

正常情况下我们运行以上代码会报错,运行结果:

Lua协同程序coroutine详解_第7张图片

我们会被提醒i为空不能被打印的错误消息。但如果我们将以上代码放到协同程序中会怎样呢?

local co = coroutine.create(function() 
    for i = 1 , 10 do
        i = nil 
        print("我是for循环 " .. i)
    end
end)
coroutine.resume(co)

运行结果:

 

我们发现lua并没有显示错误消息。因此,这一点也提醒我们在开发中使用协同程序一定要慎重,因为一旦协同程序中出现错误并不好定位。


6.当一个协同程序A唤醒另一个协同程序B时,协同程序A就处于一个特殊状态,即不是挂起状态(无法继续A的执行),也不是运行状态(是B在运行)。所以将这个状态称为“正常”状态

 

7.Lua的协同程序还有一项有用的机制,就是可以通过一对resume-yield来交换数据。一般调用一个yield()挂起协同程序后都会有一个resume()恢复协同程序与之对应。但在第一次调用resume()时,并没有对应的yield()在等待它,因此所有传递给resume()的额外参数都将视为协同程序主函数的参数

local co = coroutine.create(
    function(a,b,c)
        print("我是协同程序 " .. a .. " " .. b .." " ..c)
    end
)
 
coroutine.resume(co,1,2,3)

运行结果:

 Lua协同程序coroutine详解_第8张图片


8.在resume()调用返回的内容时,第一个值为true则表示没有错误,而后面所有的值都是对应yield()传入的参数

local co = coroutine.create(
    function(a,b)
        coroutine.yield(a+b,a-b)
    end
)
 
print(coroutine.resume(co,20,10))

运行结果:

Lua协同程序coroutine详解_第9张图片 

这里要解释一下,首先调用resume()因为是第一次,参见第7条,向协同程序主方法中传入了参数20,10,主方法接收到参数赋值到a,b,然后在协同程序主方法中调用了yield()方法,yield()方法的参数a+b,a-b就会成为resume()方法的返回值。

9.与此对应的是,yield()返回的值就是对应resume()额外传入的参数

local co = coroutine.create(
    function(a,b)
        print("协同程序  ".. coroutine.yield())
    end
)
coroutine.resume(co)
coroutine.resume(co,100)

运行结果:

 Lua协同程序coroutine详解_第10张图片

可以看到,执行第一个resume()没有与之对应的yield(),执行第二个resume()时其额外传入的参数“100”就是yield的返回值。

注意,第8条是resume()的返回值是yield()的传入参数,而第9条是yield()的返回值是resume()的传入参数,虽然有点乱,但还是可以理解的。


10.最后,当一个协同程序结束时,它的主函数所返回的值都将作为对应resume()的返回值:

local co = coroutine.create(
    function()
        return 6,7
    end
)
 
print(coroutine.resume(co))

运行结果:

 Lua协同程序coroutine详解_第11张图片

 11.看到这里我估计你可能已经懵了,第8条明明说resume()的返回值是yield()的传入参数,现在第10条为什么又说resume()的返回值是主函数所返回的值呢,矛盾的啊?

Talk is cheap ,show me the code,我们接下来就写个即满足第8条又满足第10条的代码看看会发生什么:

local co = coroutine.create(  
    function(a,b) 
        coroutine.yield(a+b,a-b) 
        return 6,7  
    end  
)  

print(coroutine.resume(co,20,10))   
print(coroutine.resume(co)) 
print(coroutine.resume(co))

运行结果:

 Lua协同程序coroutine详解_第12张图片

我们可以看到,第一次执行resume()的返回值是yield()传入的参数,第二次执行resume()的返回值是主函数所返回的值,第三次执行resume()此时协同程序内容已执行完毕,已处于死亡状态,返回false和一条错误消息。

所以第8条和第10条并不矛盾,当协调程序里面有yield()时,调用resume()会在yield()处挂起,并且yield()传入的参数会作为此次resume()的返回值;当协同程序中调用return执行完毕后,此次恢复该协同程序的resume()会把return的返回值作为自己的返回值,有点绕哈大家慢慢捋一捋。






以上就是lua协同程序的相关内容,详细内容可参看《lua程序设计》(第二版),博主理解如有偏颇,还请指正

 

 

 

你可能感兴趣的:(Lua)