快速掌握Lua 5.3 —— Coroutines

Q:什么是”Coroutine”?

A:有些类似于多线程,但他们之间也有区别,
1、从概念上来看,多线程是同一时间所有的线程同时都在运行。而一组”Coroutines”在同一时间只有一个”Coroutine”在运行。
2、从应用场景来看,多线程一般起到分流的作用,每个线程专注做自己的事情,线程之间合作的关系较弱。而一组”Coroutines”之间合作的关系就比较强,他们都是在做同一件事情,他们分摊了这件事情中的工作。

Q:如何使用”Coroutines”?

A:

-- 创建"Coroutines"。
co = coroutine.create(function()
         print("hi")
     end)
print(type(co))    --> thread    -- "Coroutine"是个"thread"。

-- "Coroutine"有3种状态:挂起,运行,死亡。
-- 当"Coroutine"被创建之后默认是挂起状态。
print(coroutine.status(co))    --> suspended
-- 运行"Coroutine"。
coroutine.resume(co)    --> hi
-- "Coroutine"在运行完成之后变为死亡状态。
print(coroutine.status(co))    --> dead

目前看起来”Coroutine”与普通的函数调用没有太大区别,但他真正强大的地方在于coroutine.yield(),它可以让运行中的”Coroutine”挂起,

co = coroutine.create(function ()
         for i=1, 10 do
             print("co", i)
             coroutine.yield()    -- 挂起"Coroutine"。
         end
     end)
coroutine.resume(co)    --> co   1
coroutine.resume(co)    --> co   2
coroutine.resume(co)    --> co   3
...
co)    --> co   10
coroutine.resume(co)    -- prints nothing -- "for"循环结束。

以及coroutine.resume()coroutine.yield()之间可以方便的交换数据,

-- 可以通过"coroutine.resume()"向"coroutine"调用的函数传递参数。
co = coroutine.create(function(a, b)
         print("co", a, b)
     end)
coroutine.resume(co, 1, 2)    --> co  1  2

--[[ 可以通过"coroutine.yield()"向"coroutine.resume()"传递参数。
     "coroutine.yield()"传递的参数由"coroutine.resume()"的返回值呈现。]]
co = coroutine.create(function(a, b)
         for i = 1, 3 do
             print(i, a, b)
             coroutine.yield(i, a + b, a - b)
         end
     end)
print(coroutine.resume(co, 20, 10))
print(coroutine.resume(co, 50, 10))
print(coroutine.resume(co, 70, 90))
--[[ 结果。
     这里可以看到,第一次"coroutine.resume()"所传递的参数,
     将作为之后每次"coroutine.resume()"所传递的参数,
     无论之后的"coroutine.resume()"是否指定了新的参数。]]
1   20  10
true    1   30  10
2   20  10
true    2   30  10
3   20  10
true    3   30  10

--[[ 对称的,可以通过"coroutine.resume()"向"coroutine.yield()"传递参数。
     "coroutine.resume()"传递的参数由"coroutine.yield()"的返回值呈现。]]
co = coroutine.create (function(a, b)
         for i = 1, 3 do
             print(i, a, b)
             print("co", coroutine.yield())
         end
     end)
coroutine.resume(co, "first", "call", "discard")
coroutine.resume(co, "second", "call")
coroutine.resume(co, "third", "call")
coroutine.resume(co, "fourth", "call")
--[[ 结果。
     这里可以看到,
     1、从创建"coroutine"之后的第一次"coroutine.resume()"
     传递的多余参数被丢弃了。
     2、与上面的例子相同,第一次"coroutine.resume()"所传递的参数,
     将作为之后每次"coroutine.resume()"所传递的参数,
     无论之后的"coroutine.resume()"是否指定了新的参数。
     3、第一次之后的"coroutine.resume()"所传递的参数
     都会被视作传递给"coroutine.yield()"的参数。
     4、当"coroutine"被挂起时,"coroutine.yield()"并不返回,
     猜测可能是在"coroutine.yield()"内部暂停了。
     之后当"coroutine"被恢复时,"coroutine.yield()"才返回,
     猜测可能是从"coroutine.yield()"内部继续运行到"coroutine.yield()"结束返回。
     以上的现象与"coroutine"的运行流程相对应,
     在创建"coroutine"之后的第一次"coroutine.resume()"
     是从创建"coroutine"时指定的函数开始运行,
     之后的每次"coroutine.resume()"
     是从"coroutine.yield()"内部暂停的地方开始运行。]]
1   first   call
co  second  call
2   first   call
co  third   call
3   first   call
co  fourth  call

--[[ 当"coroutine"转换为死亡状态时,
     创建"coroutine"时所指定的函数所返回的值也会传递给"coroutine.resume()",
     由"coroutine.resume()"的返回值呈现。]]
co = coroutine.create(function ()
         for i = 1, 3 do
             coroutine.yield()
         end

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

Q:使用”Coroutines”的例子?

A:协同程序的一个典型例子是生产者和消费者问题。比如一个函数不断产生值(从一个文件中读取数据,此例中是从标准输入中读取),另一个函数不断消耗值(将数据写到另一个文件中,此例中是向标准输出写)。

function receive ()
    local status, value = coroutine.resume(producer)
    return value
end

function send (x)
    coroutine.yield(x)
end

function consumer ()
    while true do
        local x = receive()        -- receive from producer
        io.write(x, "\n")          -- consume new value
    end
end

producer = coroutine.create(
function ()
    while true do
        local x = io.read()     -- produce new value
        send(x)                 -- send to consumer
    end
end)

consumer()     -- 从消费者开始。
--[[ 消费者写数据需要先接收数据,调用"receive()",
     "receive()"内部会恢复"coroutines"的运行,并等待数据,
     这样主导权到了生产者手中,
     生产者读取数据并发送数据,调用"send()""send()"内部挂起"coroutines",
     此时数据通过"coroutine.yield()"传递给了"coroutine.resume()""coroutine.resume()"返回数据给"receive()",写数据。
     循环往复。]]

我们可以过滤器扩展这个例子,过滤器在生产者与消费者之间,可以对数据
进行某些转换处理。过滤器在同一时间既是生产者,也是消费者。说的更具体一些,过滤器在同一时间,对于真正的生产者,它是消费者,对于真正的消费者,它是生产者。
我们修改上面的例子,加入打印行号的功能,

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()
            -- 挂起producer(即自己)所在的"coroutines",返回给filter数据。
            send(x)
        end
    end)
end

function filter (prod)
    return coroutine.create(function ()
        local line = 1
        while true do
            -- 恢复producer所在的"coroutines",以让producer提供数据。
            local x = receive(prod)
            x = string.format("%5d %s", line, x)    -- 行号。
            -- 挂起filter(即自己)所在的"coroutines",返回给consumer数据。
            send(x)
            line = line + 1
        end
    end)
end

function consumer (prod)
    while true do
        -- 恢复filter所在的"coroutines",以让filter提供数据。
        local x = receive(prod)
        io.write(x, "\n")
    end
end

--[[ "producer()"创建了一组"coroutines",由filter掌控;
     "filter()"创建了一组"coroutines",由consumer掌控。]]
consumer(filter(producer()))

上面这个例子可能很自然的让你想到”UNIX”的管道。管道的方式下每一个任务在独立的进程中运行,而”coroutines”方式下每个任务运行在独立的”coroutine”中。进程之间的切换代价很高,而”coroutine”的切换的代价大致相当于函数之间的切换。

Q:如何使用”Coroutines”实现”Iterator”?

A:”Iterator”是一种典型的“生产者 - 消费者”模式,由”Iterator”产生值,由循环体消耗值。下面是一个打印数组内元素全排列的例子,我们先用递归来实现它,

-- 算法很简单,就是依次将数组中每一个元素放到数组的最后,然后计算余下元素组成的数组的全排列。
function permgen (a, n)
    if n == 0 then
        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,v in ipairs(a) do
        io.write(v, " ")
    end
    io.write("\n")
end

permgen ({1,2,3,4}, 4)

接下来,将其转换为”Coroutines”的实现方式就很简单了,

function permgen (a, n)
    if n == 0 then
        -- 一旦得到排列结果,"coroutine"挂起,返回结果给"coroutine.resume()"。
        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

-- 内部创建"coroutine"以及返回"iterator function"。
function perm (a)
    local n = #a    -- 获得数组的大小。
    -- "coroutine"负责以递归的方式产生排列结果。
    local co = coroutine.create(function () permgen(a, n) end)
    -- "iterator function"负责恢复"coroutine"以从"coroutine.yield"得到排列结果。
    return function ()
        local code, res = coroutine.resume(co)
        return res
    end
end

function printResult (a)
    for i,v in ipairs(a) do
        io.write(v, " ")
    end
    io.write("\n")
end

for p in perm{"a", "b", "c"} do
    printResult(p)
end

perm()使用了Lua中一种常用的模式:将恢复”coroutine”的操作封装在一个函数里。这种模式在Lua中经常被使用,所以Lua提供coroutine.wrap()来实现这种操作。与coroutine.create()相比,coroutine.create()返回”coroutine”本身,而coroutine.wrap()在内部调用coroutine.resume()后,等待coroutine.resume()返回并返回其结果。当coroutine.resume()成功恢复了”coroutine”时,coroutine.wrap()返回coroutine.resume()除了第一个返回值(即”errcode”,true or false)外的其他返回值。而当coroutine.resume()恢复”coroutine”失败时,coroutine.wrap()直接报错。
所以,我们可以将原先perm()的实现更改为使用coroutine.wrap()

function perm (a)
  local n = #a
  return coroutine.wrap(function () permgen(a, n) end)
end

Q:如何使用”coroutines”实现抢占式线程?

A:”coroutines”是非抢占式的协作线程,这意味着在明确的指明要他停止(通过调用coroutine.yield())之前,他不能被其他线程打断,这种机制在某些需要高实时性的场合是不能被接收的。
抢占式线程的“抢占”其原理就在于CPU为每个线程分配一个时间片,当时间片耗尽而线程的工作还没有完成时,CPU就强制将该线程暂停同时让另一个线程开始工作。
使用”coroutines”也可以实现抢占式线程,其原理就在于由我们自己为每一对”coroutine”分配时间片。以下的例子简单的实现了抢占式线程,每个线程的工作都是计算某个整数区间内所有整数之和,

threads = {}    -- 调度表。所有工作中的线程都会存入此表。
time = os.time()    -- 每个线程开始工作时的时间。当线程被挂起时会更新这个时间。
limit_time = 1    -- 时间片。每个线程给1s的工作时间。

-- 计算某个整数区间内所有整数之和。
function cal(from, to)
    local sum = 0;
    for i = from, to do
        sum = sum + i
        -- 时间片耗尽,而工作还没有完成。
        if (os.time() - time) >= limit_time then
            -- 打印计算进度。
            print(string.format("Worker %d calculating, %f%%.", worker, (i / to * 100)))
            time = os.time()    -- 当进程被挂起时更新时间,下一个进程将以此作为开始工作时的时间。
            coroutine.yield()    -- 休息。
        end
    end
    -- 工作完成,打印计算结果。
    print(string.format("Worker %d finished, %d.", worker, sum))
end

-- 分配任务。
function job (from, to)
    -- 创建"coroutine"。
    local co = coroutine.create(function ()
        cal(from, to)
    end)
    table.insert(threads, co)    -- 将线程加入调度表。
end

-- 4个线程,分别计算不同整数区间内所有整数之和。
job(1, 100000000)
job(10, 50000000)
job(5000, 6000000)
job(10000, 70000000)

-- 分发器。调度所有线程的运行。
while true do
    local n = #threads
    if n == 0 then break end   -- 没有线程需要工作了。
    for i = 1, n do
        worker = i    -- 表示哪个线程在工作。
        local status = coroutine.resume(threads[i])    -- 恢复"coroutine"工作。
        if not status then    -- 线程是否完成了他的工作?"coroutine"完成任务时,status是"false"。
            table.remove(threads, i)    -- 将线程从调度表中删除。
            break
        end
    end
end

Worker 1 calculating, 0.482016%.
Worker 2 calculating, 10.191070%.
Worker 3 calculating, 85.029767%.
Worker 4 calculating, 7.273926%.
Worker 1 calculating, 5.534152%.
Worker 2 calculating, 20.370044%.
Worker 3 finished, 17999990502500. <– 3号线程完成工作。
Worker 4 calculating, 13.201869%.
Worker 1 calculating, 10.590240%.
Worker 2 calculating, 30.563072%.
Worker 1 calculating, 15.656026%.
Worker 2 calculating, 40.731870%.
Worker 3 calculating, 20.430621%. <– 原先的4号线程变为3号线程。
Worker 1 calculating, 20.689852%.
Worker 2 calculating, 50.912444%.
Worker 3 calculating, 27.682421%.
Worker 1 calculating, 25.750115%.
Worker 2 calculating, 61.074508%.

实现的思路也很清晰。一个调度器拥有一张调度表,其中存储了需要工作的线程。调度器为每个线程分配一个时间片,当发现线程的时间片耗尽,则挂起线程同时让调度表中的下一个开始工作。当发现线程的工作完成时,则从调度表中移除该线程。总的来说,通过调度器与时间片,使用”coroutines”实现了简单的抢占式线程。

附加:

1、coroutine.resume()会返回一个”bool”值,表示是否成功的恢复了挂起的”Coroutine”,

-- 处于挂起状态的"Coroutine"可以恢复。
print(coroutine.status(xxx))    --> suspended
print(coroutine.resume(xxx))    --> true
-- 已处于死亡状态的"Coroutine"无法恢复。
print(coroutine.status(xxx))    --> dead
print(coroutine.resume(xxx))    --> false   cannot resume dead coroutine

2、在生产者和消费者问题的第一个例子中,
开始时调用消费者,当消费者需要值时他唤起生产者生产值,生产者生产值后停止直到消费者再次请求。我们称这种设计为消费者驱动的设计。
3、”coroutines”是一种协作的多线程。每一个”coroutine”相当于一个线程。”yield-resume”的组合可以在线程之间互相转换。然而,区别于真正的多线程,”coroutines”是非抢占的。当一个”coroutine”在运行的时候,他不能被其他的线程所打断,除非明确的指明要他停止(通过调用coroutine.yield())。编写非抢占式的多线程也比编写抢占式的多线程容易的多,因为你不用担心线程之间的同步问题所造成的bugs,你只需要确定当”coroutine”被挂起时他不是处在临界区。

你可能感兴趣的:(lua,快速掌握Lua,5.3,lua,线程)