Lua学习笔记 第九章 协同程序 —— coroutine

协同程序与线程差不多,拥有自己独立的栈、局部变量和指令指针,同时又与其它协同程序共享

全局变量和其它大部分东西。线程与协同程序的主要区别:

一个具有多个线程的程序可以同时运行几个线程;而一个具有多个协同程序的程序在任意时刻只

能运行一个协同程序,并且正在运行的协同程序只会在其显示地要求挂起时,它的执行才会暂停。

 

9.1 协同程序基础

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

函数create用于创建新的协同程序,它只有一个参数,就是一个参数。该函数的代码就是

协同程序所需执行的内容。create会返回一个thread类型的值,用以表示新的协同程序。

通常create的参数是一个匿名函数。示例:

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

print(co) --> thread:ox8071d98

一个协同程序可以处于4种不同的状态:

挂起(suspended)、运行(running)、死亡(dead)和正常(normal)

当创建一个协同程序时,它处于挂起状态,而不会自动执行其内容。

函数status来检查协同程序的状态:

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

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

运行; coroutine.resume(co) --> hi

协同程序终止后就处于死亡状态,再也无法返回了:

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

函数yield可以让一个运行的协同程序挂起,而之后可以再恢复它的运行。示例:

co = coroutine.create(function()

    for i=1, 10do

        print("co", i)

        oroutine.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) --> co 10

coroutine.resume(co) --> 什么都不打印

 

最后一次调用resume时,协同程序的内容已经执行完毕,并已经返回。因此,这是协同

程序处于死亡状态,如果试图再次恢复它的执行,resume将返回false及一条错误消息:

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

注意: resume是在保护模式中运行的。因此如果在一个协同程序的执行中发生任何错误,

Lua是不会显示错误消息的,二是将执行权返回给resume调用。

当一个协同程序A唤醒另一个协同程序B时,协同程序A就处于一个特殊的状态,既不是

挂起状态也不是运行状态,这时的状态称为"正常"状态。

Lua协同程序的一种有用机制,通过一对resume-yield来交换数据。

在第一次调用resume时,并没有对应的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

resume调用返回的内容中,第一个值为true则表示没有错误,而后面所有的值都是对应

yield传入的参数:

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

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

end)

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

与此对应的是,yield返回的额外值就是对应resume传入的参数:

co = coroutine.create(function()

    print("co",coroutine.yield())

end)

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

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

co = coroutine.create(function()

        return 6, 7

end)

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

: 很少在一个协同程序中使用所有这些功能,但每种功能各有其用途。

 

Lua提供的协同程序是一种"非对称的协同程序(asymmetric coroutine)",也就是说Lua提供

了两个函数来控制协同程序的执行,一个用于挂起执行,另一个用于恢复执行。在这种实现

中,一个协同程序只能在它没有调用其它函数时(它的调用栈中没有未完成的调用),才可以

挂起执行。换句话说,只有协同程序的主函数才能调用类似于yield这样的函数。

 

9.2 管道(pipe)与过滤器(filter)

关于协同程序的经典示例就是"生产者-消费者"的问题。这其中涉及到两个函数。一个函数

不断地产生值,另一个则不断地消费这些值。

协同程序被称为是一种匹配生产者和消费者的理想工具,一对resume-yield完全一改典型

的调用者与被调用者之间的关系。当一个协同程序调用yield时,它不是进入了一个新的函数

而是从一个悬而未决的resume调用中返回。同样地,对于resume的调用也不会启动一个新函数,

而是从一次yield调用中返回,这项特征可用于匹配sendreceive,这两者都认为自己是主动方,

对方是被动方。receive唤醒生产者的执行,促使其产生一个新值。而send则产出一个新值返还给消费者:

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()

            send(x)

        end

end)

在这种设计中,程序通过调用消费者来启动。当消费者和需要一个新值时,它唤醒生产者。

生产者返回一个新值后停止运行,并等待消费者的再次唤醒。这种设计被称为"消费者驱动

—— consumer-driven".

可扩展上述设计,实现过滤器(filter).过滤器是一种位于生产者和消费者之间的处理功能,

可用于对数据的一些变换。过滤器及时一个消费者又是一个生产者,它唤醒一个生产者

促使其产生新值,然后又将变换后的值传递给消费者。示例:在前面代码中添加一个过滤器,

在每行起始处插入一个行号——

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() --产生新值

                send(x)

            end

end)

end

 

function filter(prod)

    returncoroutine.create(function()

            for line = 1, math.huge do

                local x = receive(prod)  -- 获取新值

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

                send(x) -- 将新值发送给消费者

            end

        end)

end

function consumer(prod)

    while true do

        local x =receive(prod) -- 获取新值

        io.write(x,"\n")

    end

end

启动消费者的方式:

p = producer()

f = filter(p)

consumer(f) 或者 consumer(filter(producer()))

 

9.3 以协同程序实现迭代器

将循环迭代器视为"生产者—消费者"模式的一种特例,一个迭代器会产出一些内容,而循环体

则会消费这些内容。因此,协同程序也适用于实现迭代器。

示例迭代器 —— 遍历某个数组的所有排列组合形式。

第一步:编写一个递归函数来产生所有的排列组合形式。想法 —— 将每个数组元素一次放到

最后一个位置,然后递归生成其余元素的排列。代码:

function permgen(a, n)

    n = n or #a

    if n <= 1then

printResult(a)

    else

        for i=1,n do                   -- 将第i个元素放到数组末尾

            a[n],a[i] = a[i], a[n]    -- 生成其余元素的排列

            permgen(a,n-1)       --恢复第i个元素

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

        end

    end

end

function printResult(a)

    for i = 1, #ado

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

    end

    io.write("\n")

end

调用:permgen({1,2,3,4})

第二步:函数完成后,将其转换为一个迭代器。

首先,将printResult改为yield

function permgen(a, n)

    n = n or #a

    if n <= 1then

        coroutine.yeild(a)

    else

        <asbefore>

然后,定义一个工厂函数,用于将生成函数放到一个协同程序中运行,并创建迭代器函数。

迭代器只是简单地唤醒协同程序,让其产生下一种排列:

function permutations(a)

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

    returnfunction()

        localcode, res = coroutine.resume(co)

        returnres

    end

end

最后,在for语句中遍历一个数组的的所有排列。

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

printResult(p)

end

permutations函数使用了一种在Lua中比较常见的模式,就是将一条唤醒协同程序的调用

包装在一个函数中。了解coroutine.wrap

 

9.4 非抢先式的多线程

协同程序提供了一种协作式的多线程,每个协同程序都等于是一个线程。一对yield-resume

可以将执行权在不同线程之间切换。然而,协同程序与常规的多线程的不同之处在于,协同

程序时非抢先式的。就是说,当一个协同程序在运行时,是无法从外部停止它的。只有当协

同程序显示地要求挂起时(调用yield),它才会停止。

对于非抢先式的多线程来说,只要有一个线程调用了一个阻塞的操作,整个程序在该操作

完成前,都会停止下来。对于很多应用程序来说,这种行为是无法接受的。解决方法,

见示例——希望通过HTTP下载几个远程文件。

下载一个文件的步骤:

第一步,加载LuaSocket库,require"socket"

第二步,定义主机和下载的文件。如

host="www.w3.org"

        file = "/TR/REC-html32.html"

第三步,打开一个TCP连接,连接到该站点的80端口。

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

        这步操作返回一个连接对象,可以用他来发送文件请求。

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

第四步,按1k的字节块来接收文件,并将每块写到标准输出。

while truedo

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

io.write(sorpartial)

if status =="closed" then break end

        end

在正常情况下receive函数会返回一个字符串。若发生错误,则会返回nil 并且附加错误

代码(status) 及出错前读取到的内容(partial).当主机关闭连接时,就将其余接收到的内容

打印出来,然后退出接收循环。

第五步,下载完文件后,关闭连接。c:close()

 

下载多个文件:

如果逐个下载文件,这种做法太慢。当接收一个远程文件时,程序将大部分时间花费在等待

数据接收上,更明确地说,是将时间用在了对receive的阻塞调用上。因此如果一个程序可以

同时下载所有文件的话,那么它的运行速度就快很多。想法 -- 为每个下载任务创建一个新的线程,

只要一个线程无可用数据,它就可以讲控制权转让给一个简单的调度程序,而这个调度程序则会

调用其它的下载线程。

将之前的下载代码封装为一个函数,代码如下:

function download(host, file)

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

    local count =0         --记录接收到的字节数

    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

辅助函数receive来从连接接收数据,在顺序下载的方法中receive的代码可以这样写:

function receive(connection)

    returnconnection:receive(2^10)

end

在并发的实现中,这个函数在接收数据时绝对不能阻塞。它需要在没有足够的可用数据时挂起执行。

代码如下:

function receive(connection)

    connection:settimeout(0)  --使recevie调用不会阻塞

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

    if status =="timeout" then

        coroutine.yield(connection)

    end

    return s orpartial, status

end

settimeout(0)的调用可使以后所有对此连接进行的操作不会阻塞。若一个操作返回的statustimeout(超时)

就表示该操作在返回时还未完成。此时线程就会挂起执行。

调度程序及一些辅助代码。table thread 为调度程序保存着所有正在运行中的线程。函数get确保每个下载任务

都在一个独立的线程中执行。调度程序本身就是一个循环,它遍历所有的线程,逐个唤醒它们的执行。并且

当线程完成任务时,将该线程从列表中删除。在所有线程都完成运行后,停止循环。

thread = {}

function get(host, file)

    local co =coroutine.create(function() download(host, file) end)

    table.insert (threads,co)

end

function dispatch()

    local i = 1

    while true do

        if threads[i] == nil then

            ifthreads[1] == nil then break end

            i = 1

        end

        localstatus, res = coroutine.resume(thread[i])

        if notres then

            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/DCM2-Core.txt")

dispacth() -- 主循环

 

上述的的实现不够完美,如果所有线程都没有数据可读,调度程序就会执行一个"忙碌等待"

不断地从一个线程切换到另一个线程。这样便导致这个协同程序的实现会比顺序下载多耗费

30倍的CPU时间。解决方法是用LuaSocket中的select函数。这个函数可以用于等待一组socket

的状态改变,在等待时陷入阻塞状态。若要在当前实现中应用这个函数,只需修改调度程序即可。

新版本如下:

function dispatch()

    local  i = 1

    local  connections = {}

    while true do

        if  threads[i] == nil then

            if  threads[1] == nil then break end

            i = 1

    connections= {}

        end

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

        if not res then

    table.remove(threads,i)

        else

            i = i+ 1

            connections[#connections + 1] = res

            if  #connections == #thread then

                socket.select(connections)

            end

        end

    end

end


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值