A:有些类似于多线程,但他们之间也有区别,
1、从概念上来看,多线程是同一时间所有的线程同时都在运行。而一组”Coroutines”在同一时间只有一个”Coroutine”在运行。
2、从应用场景来看,多线程一般起到分流的作用,每个线程专注做自己的事情,线程之间合作的关系较弱。而一组”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
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”的切换的代价大致相当于函数之间的切换。
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
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”被挂起时他不是处在临界区。