Programming in Lua, 2nd edition - Chapter 9: Coroutines

本文深入探讨Lua中的协程概念,包括基本用法、生产者-消费者模型的应用、作为迭代器的功能以及非抢占式多线程的实践案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

 

 

Chapter 9: Coroutines

 

协程是一种非抢占式多线程。当一个协程运行时,没有办法从外部将其停止,只有等它自已主动进入休眼状态(调用yield)或自行终止对协程来说,各个任务运行于独立的协程中在协程间进行切换的代价大体上和函数调用相当。(协程的运行过程是执行-暂停、执行-暂停,

 

协程与线程类似:是一串操作,有自已的栈、局部变量和指令指针;但是与其它协程共享全局变量和几乎其它程何东西。 协程和线程的主要不同是,线程是并发执行的(在多核环境下),协程不是

 

协程非常有用,也比较复杂。

 

9.1 Coroutine Basics

 

Lua 将与协程相关的函数都封装在coroutine 表中。coroutine 表的create 函数创建新协程。create 的参数是欲运行的函数。

 

co = coroutine.create(function () print("hi") end)

print(co) --> thread: 0x8071d98

 

一个协程可以有四种状态:挂起、运行中、死亡和正常。

 

刚创建的协程处于挂起状态

 

检查协程的当前状态

 

print(coroutine.status(co)) --> suspended

 

激活resume协程

 

coroutine.resume(co) --> hi  

 

上面的代码先打印hi,然后进入dead 状态,这之前协程不会返回

 

print(coroutine.status(co)) --> dead

 


 

协程的强大源于yield 函数

 

co = coroutine.create(function ()

       for i=1,3 do

              print("co", i)

              coroutine.yield()               -- yield 函数主动挂起协程

       end

end)

 

coroutine.resume(co)                         --> co 1

print(coroutine.status(co))                   --> suspended

coroutine.resume(co)                         --> co 2

coroutine.resume(co)                         --> co 3

coroutine.resume(co)               --> 这个调用导至协程变为dead 状态

print(coroutine.resume(co))                --> false  cannot resume dead coroutine

 

对状态是dead 的协程,resume 函数返回false 加上错误消息。

 

注意resume 运行于保护模式pcall?)。因为这使得协程内部出错时能够有机会显示错误消息。

 

当一个协程激活resumes 另一个协程,它不会挂起也不是运行状态,而是正常状态(normal)

 

一个有用的特性是,在一对resum yield 之间可以交换数据

 

 

给协程的主函数传递参数,主函数的返回值又传回resume

 

co = coroutine.create(function (a,b,c)

print("co", a,b,c)

end)

coroutine.resume(co, 1, 2, 3) --> co 1 2 3    -- 打印完后,进入dead 状态(那个匿名函数运行完了)

 


 

yield 函数计算传给它的参数表达式,并返回true 加结果。

 

co = coroutine.create(function (a,b)

coroutine.yield(a + b, a - b)

end)

print(coroutine.resume(co, 20, 10))     --> true 30 10

print(coroutine.resume(co, 20, 10))     --> true

print(coroutine.resume(co, 20, 10))     --> false  cannot resume dead coroutine

 

 

不经过协程的主函数,直接给yield 传递参数

 

co = coroutine.create (function ()

                   print("co", coroutine.yield())   -- yield() 第一次暂停,第二次返回4,5 然后协程dead

         end)

coroutine.resume(co)

coroutine.resume(co, 4, 5) --> co 4 5

 

 

最后,当一个协程终上,任何由它的主函数返回的值都传到相应的resume

(主函数既传给create 的那个函数)

co = coroutine.create(function ()

       return 6, 7

  end)

 

print(coroutine.resume(co)) --> true 6 7

 

 

我们很少在同一个协程中使用所有这些特性,但它们各有各的用处。

 

在我们继续深入之前先澄清一些概念是重要的。Lua 提供一种称为asymmetric coroutines 的机制(非对称协程)。意思是,它有一个函数挂起一个协程的执行,还有另一个函数恢复协程的执行。其它一些语言提供symmetric coroutines(对称协程),它们只用一个函数来控制从一个协程到另一个协程的转换。

 

一些人管非对称协程叫semi-coroutines。但是其它人也用同样的术语称呼协程的受限实现,这种实现中只有当一个协程不调用任何函数时能挂起自已。Python generator 就是一个semi-coroutines 的例子

 

 

9.2 Pipes and Filters

 

协程中最特别的一个例子是生产者消费者问题。让我们假设我们有一个函数,它不断的生产值(例如,读一个文件),而另一个函数不断的消费这些值(例如,把这些值写入另一个文件)。

 

产者消费者问题

 

function producer ()

while true do

local x = io.read()              -- produce new value

send(x)                                -- send to consumer

end

end

function consumer ()

while true do

local x = receive()              -- receive from producer

io.write(x, "/n")        -- consume new value

end

end

 

(这个例子中生产者和消费者都永运不停的运行,容易改成当没有要处理的数据时停止)这里的问题是如何协调send receive(比如它们的速度)。这是一个典型的谁拥有主循环的问题

对生产者和消费者来说,两者都是活动的,都有主循环,两者都假定对方是可随时提供服务的。

 

协程为生产者消费者间的调度提供了理想的工具

 


 

消费者驱动设计

 

function receive ()

       local status, value = coroutine.resume(producer)

       return value

end

 

function send (x)

       coroutine.yield(x)

end

 

producer = coroutine.create(

       function ()

              while true do

              local x = io.read() -- produce new value

              send(x)

              end

  end)

 

当然,生产者现在必须做为一个协程。在这个设计中,程序从调用消费者开始,当消费者需要一个数据,它激活(resumes 生产者,然后生产者一直运行,一旦生产出一个给消费者的东西就休眠(suspended),直到再一次被消费者激活。所以,这个设计称为消费者驱动设计(consumer-driven)。

 

我们可以用过滤器filters)扩展这个设计,过滤器在生产者和消费者之间做一些数据处理的任务。过滤器激活生产者得到一个新值,进行适当转换后把它传给消费者。

 

 

过滤器

 

function receive (prod)

       local status, value = coroutine.resume(prod)

       return value

end

 

function send (x)

       coroutine.yield(x)

end

 

function producer ()

       return coroutine.create(function ()

              while true do

                     local x = io.read() -- produce new value

                     send(x)

              end

      end)

end

 

function filter (prod)

       return coroutine.create(function ()

              for line = 1, math.huge do

                     local x = receive(prod) -- get new value

                     x = string.format("%5d %s", line, x)

                     send(x) -- send it to consumer

              end

         end)

end

 

function consumer (prod)

       while true do

              local x = receive(prod) -- get new value

              io.write(x, "/n") -- consume new value

       end

end

 

p = producer()

f = filter(p)

consumer(f)

------------------------------

-- consumer(filter(producer()))  -- 也可以这样

 

程序先创建一个协程,这个协程就是生产者。再创建一个协程,这个协程就是过滤器,将生产者作为参数传给它。将过虑器作为参数传给消费者。消费者在一个无限循环里面不断的消费产品。每一次消费产品的过程是:消费者先resume激活过虑器,然后过虑器resume激活生产者,它生产一个产品(从什么地方读一个数据),并放在yield 里面就挂起了。过虑器从resume 函数得到产品并进行处理(格式化从io.read获得的string,在前面加上行号做为新产品),将新产品放在yield 里面然后挂起。最后消费者从resume 函数得到最终产品。处理完产品后(将串打印出来),新一轮消费又开始了。

 


 

看过前面协程的例子会让人想起Unix 的管道。毕竟,协程是多线程的一种(非抢占式的)

对管道来说,各个任务运行于独立的进程中,而对协程来说,各个任务是运行于独立的协程中。管道为读和写(既生产者与消费者)提供了一个缓冲区,所以他们之间的相对速度获得了一定的自由。在管道的环境中这是重要的,因为在进程间进行切换代价高昂。而在协程间进行切换的开销更小,大体上和函数调用相当。所以,写入器和读取器可以交替运行。

 


 

9.3 Coroutines as Iterators

 

我们可以将迭代循环看成是生产者-消费者模型的一个特例

 

迭代器产生Item 供循环体消费。因此,看起来用协程来写迭代器是合适的。当然,协程为此任务提供了一个强有力的工具。

 

输出一串数字的全排列(为协程迭代器作准备工作)

 

function permgen (a, n)

       n = n or #a -- default for 'n' is size of 'a'

       if n <= 1 then -- nothing to change?

              printResult(a)

       else

              for i=1,n do

                     -- put i-th element as the last one

                     a[n], a[i] = a[i], a[n]

                     -- generate all permutations of the other elements

                     permgen(a, n - 1)

                     -- restore i-th element

                     a[n], a[i] = a[i], a[n]

              end

       end

end

 

function printResult (a)

       for i = 1, #a do

              io.write(a[i], " ")

       end

       io.write("/n")

end

 

function printResult (a)

       for i = 1, #a do

              io.write(a[i], " ")

       end

       io.write("/n")

end

permgen ({1,2,3,4})  -- 表不要太大了,不然运行起来像无限循环

 

 

这样,我们就有了一个生成器(生成全排列的生成器),接下来的任务是将这个生成器转换成迭代器。首先,我们在permgen 中应用yield

 

function permgen (a, n)

n = n or #a

if n <= 1 then

coroutine.yield(a)

else

<as before>  -- 后面的代码和之前一样

 

然后,我们定义一个工厂,它整理生成器,运行内部的协程,然后创建一个迭代函数。生成器只是简单的激活resume 协程来产生下一个排列。

 

 

生成全排列的协程迭代器

 

function permutations (a)

         local co = coroutine.create(function () permgen(a) end)

         return function () -- iterator

                   local code, res = coroutine.resume(co)

                   return res

           end

end

 

function permgen (a, n)

         n = n or #a -- default for 'n' is size of 'a'

         if n <= 1 then -- nothing to change?

                   coroutine.yield(a)

         else

                   for i=1,n do

                            -- put i-th element as the last one

                            a[n], a[i] = a[i], a[n]

                            -- generate all permutations of the other elements

                            permgen(a, n - 1)

                            -- restore i-th element

                            a[n], a[i] = a[i], a[n]

                   end

         end

end

 

function permutations (a)

         local co = coroutine.create(function () permgen(a) end)

         return function () -- iterator

                   local code, res = coroutine.resume(co)

                   return res

           end

end

 

function printResult (a)

         for i = 1, #a do

                   io.write(a[i], " ")

         end

         io.write("/n")

end

 

for p in permutations{"a", "b", "c"} do

         printResult(p)

end

 

-----------------------------------------------------------------

function permutations (a)

       return coroutine.wrap(function () permgen(a) end)

end

 

 

coroutine.wrap create 一样,创建一个新协程。不同的是,它不返回协程本身,而是返回一个函数,被调用时激活resumes 协程。

 

coroutine.wrap 不返回错误码作为第一个返回值,而是直接引发错误。

 

通常,coroutine.wrap coroutine.create 更易使用。它从协程给我们真正想要的:resume 一个函数。但是,其灵活性较低。没有办法检查使用wrap 建创的协程的状态。此外,不能检查运行时错误。

 


 

9.4 Non-Preemptive Multithreading

 

协程是一种非抢占式多线程。当一个协程运行时,没有办法从外部将其停止,只有等它自已主动进入休眼状态(调用yield)或自行终止

 

应用程序中缺乏抢占机制不会成为一个问题。相反,缺乏抢占机制使得编程更容易了。你无需担心同步bug。你只需确保协程只在出了边界时休眠yields

 

协程正在操作时,整个程序只有等它执行完才能继续后面的工作。对多数应用程序来说,这是不可接受的行为

 

让我们考虑一个典型的多线程情形:我们想要从HTTP 下载几个远程文件。这个例子用到了由Diego Nehab 开发的LuaSocket,要下载一个文件必须建立一个到站点的连接,发送文件请求,接收文件,然后关闭连接。

 

首先,加载LuaSocket 库:

 

require "socket"

 

然后,定义要从什么主机下载什么文件(这里是HTML3.2 规范文件):

 

host = "www.w3.org"

file = "/TR/REC-html32.html"    -- HTML 3.2 Reference Specification

 

接下来打开一个到80 端口的TCP 连接:

 

c = assert(socket.connect(host, 80))

 

socket.connect 返回一个连接对象,我们通过它发送文件请求:

 

c:send("GET " .. file .. " HTTP/1.0/r/n/r/n")

 

下一步,我们分块读取文件,每次读1K,将每一块写到标准输出:


 

while true do

local s, status, partial = c:receive(2^10)

io.write(s or partial)

if status == "closed" then break end

end

 

receive 返回读到的string nil(出错时),后一种状况会同时返回错误码(status) partial 是出错时读到的东西。

 

最后关闭连接:

 

c:close()

 

下载单个文件

 

require "socket"

 

host = "www.w3.org"

file = "/TR/REC-html32.html"    -- HTML 3.2 Reference Specification

 

c = assert(socket.connect(host, 80))

c:send("GET " .. file .. " HTTP/1.0/r/n/r/n")

while true do

       local s, status, partial = c:receive(2^10)

       io.write(s or partial)

       if status == "closed" then break end

end

 

c:close()

 

 

现在,我们知道了如何下载一个文件,让我们回到下载多个文件的问题上。

当读一个远程文件,程序大多数时间都花在等特数据到达。如果同时下载多个文件速度更快,当一个连接没有可用数据,程序可以从另一个连接读。协程为同时下载提供了便利的方法,我们为每一个下载任务创建一个新线程,当一个线程不再有可用的数据,它将控制权交给调度器,后者调用另一个线程。

 

 

现在receive 的实现不能再将数据分块,如果没有足够的数据,它就休眠yields

 

function receive (connection)

         connection:settimeout(0) -- do not block

         local s, status, partial = connection:receive(2^10)

         if status == "timeout" then

                   coroutine.yield(connection)

         end

         return s or partial, status

end

 

调用settimeout(0) 使得对连接的任何操作成为非阻塞操作。当操作状态是“timeout”,意思是那个操作没有完成就返回了。这种情况下,线程休眠yields传给yield non-false 参数通知调度器线程仍在执行它的任务

 

线程表为分派器保存一个所有活动线程的列表。get 函数确保每个下载运行于独立的程线。调度器本身是一个主要的循环,它遍历所有线程,一个接一个的激活它们。调度器也必须从列表里移除那些已经完成任务的线程;当不再有线程运行的时侯,调度器就终止循环。

 

最后,是程序的主要部分,它根椐需要创建线程并且调用分派器。

 

多线程下载(这个实现消耗较多的CPU时间,改进版见后面)

 

require "socket"

 

function download (host, file)

       local c = assert(socket.connect(host, 80))

       local count = 0 -- counts number of bytes read

       c:send("GET " .. file .. " HTTP/1.0/r/n/r/n")

       while true do

              local s, status, partial = receive(c)

              count = count + #(s or partial)

              if status == "closed" then break end

       end

       c:close()

       print(file, count)

end

 

function receive (connection)

       connection:settimeout(0) -- do not block

       local s, status, partial = connection:receive(2^10)

       if status == "timeout" then

              coroutine.yield(connection)

       end

       return s or partial, status

end

 

threads = {} -- list of all live threads

 

function get (host, file)

       -- create coroutine

       local co = coroutine.create(function ()

              download(host, file)

         end)

       -- insert it in the list

       table.insert(threads, co)

end

 

function dispatch ()

       local i = 1

       while true do

              if threads[i] == nil then -- no more threads?

                     if threads[1] == nil then break end -- list is empty?

                     i = 1 -- restart the loop

              end

              local status, res = coroutine.resume(threads[i])

              if not res then -- thread finished its task?

                     table.remove(threads, i)

              else

                     i = i + 1

              end

       end

end

 

host = "www.w3.org"

get(host, "/TR/html401/html40.txt")

get(host, "/TR/2002/REC-xhtml1-20020801/xhtml1.pdf")

get(host, "/TR/REC-html32.html")

get(host, "/TR/2000/REC-DOM-Level-2-Core-20001113/DOM2-Core.txt")

dispatch() -- main loop

 

 

在我的机器上使用协程下载这四个文件需要4 秒,而使用顺序下载的时间超过这个时间的两倍(15 )

 

尽管速度变快了,但离最佳实现还差很远。当至少有一个线程有数据读时一切都很好。但是,当所有线程都无事可做时,分派器还是会一个接一个的检查线程,它们仍然没有数据。结果,这个协程实现比顺序操作方安案所使用的CPU 时间要多30

 

要避免这个行为,我们可以使用LuaSocket select 函数。它允许一个程序阻塞,等待一组socket 的其中一个状态改变

 

我们只需改变分派器,新的分派器在循环中从连接表中收集“time-out”连接。如果所有连接都超时,分派器调用select 函数等待这些连接的任意一个改变状态。这个实现只比顺序操作方案使用的CPU  时间稍多。

 

多线程下载

 

require "socket"

 

function download (host, file)

       local c = assert(socket.connect(host, 80))

       local count = 0 -- counts number of bytes read

       c:send("GET " .. file .. " HTTP/1.0/r/n/r/n")

       while true do

              local s, status, partial = receive(c)

              count = count + #(s or partial)

              if status == "closed" then break end

       end

       c:close()

       print(file, count)

end

 

function receive (connection)

       connection:settimeout(0) -- do not block

       local s, status, partial = connection:receive(2^10)

       if status == "timeout" then

              coroutine.yield(connection)

       end

       return s or partial, status

end

 

threads = {} -- list of all live threads

 

function get (host, file)

       -- create coroutine

       local co = coroutine.create(function ()

              download(host, file)

         end)

       -- insert it in the list

       table.insert(threads, co)

end

 

function dispatch ()

       local i = 1

       local connections = {}

       while true do

              if threads[i] == nil then -- no more threads?

                     if threads[1] == nil then break end

                     i = 1 -- restart the loop

                     connections = {}

              end

              local status, res = coroutine.resume(threads[i])

              if not res then -- thread finished its task?

                     table.remove(threads, i)

              else -- time out

                     i = i + 1

                     connections[#connections + 1] = res

                     if #connections == #threads then -- all threads blocked?

                            socket.select(connections)

                     end

              end

       end

end

 

host = "www.w3.org"

get(host, "/TR/html401/html40.txt")

get(host, "/TR/2002/REC-xhtml1-20020801/xhtml1.pdf")

get(host, "/TR/REC-html32.html")

get(host, "/TR/2000/REC-DOM-Level-2-Core-20001113/DOM2-Core.txt")

dispatch() -- main loop

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值