第九課 協同程序

協同程序與線程差不多,也就是一條執行序列,擁有自己獨立的棧、局部變量和指令指針,同時又與其他協同程序共享全局變量和其他大部分東西。從概念上講線程與協同程序的只要區別在於,一個具有多個線程的程序可以同時運行幾個線程,而協同程序卻需要彼此協作地運行。就是說,一個具有多個協同程序的程序在任意時刻只能運行一個協同程序,並且正在運行的協同程序只會在其顯示地要求掛起時,它的執行纔會暫停。

協同程序基礎
Lua將所有關於協同程序的函數放置在一個名爲“ coroutine”的table中。
函數create用於創建新的協同程序,它只有一個參數,就是一個函數。該函數的代碼就是協同程序所需執行的內容。create會返回一個thread類型的值, 用以表示新的協同程序。通常create函數的參數是一個匿名函數,如:
co = coroutine.create(function () print("Hi") end)
print(co) -->thread:0x8071d98
一個協同程序可以處在4個不同的狀態:掛起(suspended)、運行(running)、死亡(dead)和正常(normal)。當創建一個協同程序時,它處於掛起狀態。也就是說 ,協同程序不會在創建它時自動執行其內容。可以通過函數status來檢查協同程序的狀態:
print(coroutine.status(co)) --suspended
函數coroutine.resume用於啓動或再次啓動一個協同程序的執行,並將其狀態由掛起改爲運行。
coroutine.resume(co) -->Hi
本例子中,協同程序的內容只是簡單地打印了"Hi"後便終止了,然後它就處於死亡狀態,也就再也無法返回了:
print(coroutine.status(co)) -->dead
到目前爲止,協同程序看上去還只是像一種複雜的函數調用方法。其實協同程序真正的強大之處在於函數yield的使用上,該函數可以讓一個運行中的協同程序掛起,而之後可以 再恢復它的運行。
co = coroutine.create(function ()
for i = 1, 10 do
print("co", i)
coroutine.yield()
end
end)
當喚醒這個協同程序時,它就會開始執行,直到第一個yield:
coroutine.resume(co) -->co 1
print(coroutine.status(co)) -->suspended
從協同程序的角度看,所有在它掛起時發生的活動都發生在yield調用中。當恢復協同程序的執行時,對於yield的調用才最終返回。然後協同程序繼續它的執行,直到下一個yield調用或者執行結束:
coroutine.resume(co) -->co 2
coroutine.resume(co) -->co 3
coroutine.resume(co) -->co 4
...
coroutine.resume(co) -->co 10
coroutine.resume(co) -->什麼都不打印
print(coroutine.status(co)) --dead
print(coroutine.resume(co))
-->false connot resume dead coroutine
注:resume是在保護模式中運行的。如果一個協同程序在執行中發生任何錯誤,Lua是不會顯示錯誤消息的,而是將執行權返回給resume調用。
當一個協同程序A 喚醒另一個協同程序B 時,協同程序A就處於一個特殊狀態,既不是掛起狀態(無法繼續執行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)
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提供的是一種“非對稱的協同程序”。也就是說,Lua提供了兩個函數來控制協同程序的執行,一個用於掛起執行,另一個用於恢復執行。而一些其他的語言則提供了“對稱的協同程序”,其中只有一個函數用於轉讓協同程序之間的執行權。

管道(pipe)與過濾器(filter)
生產者-消費者問題:
function producer ()
while true do
local x = io.read() --產生新值
send(x) --發送給消費者
end
end

function consumer ()
while true do
local x = receive() --從生產者接收值
io.write(x, "\n") --消費新值
end
end
如何將send與receive匹配起來。這是一個典型的“誰具有主循環(who-has-main-loop)”的問題。由於生產者和消費者都處於活動狀態,它們各自具有一個主循環,並且都將對方視爲一個可調用的服務。
協同程序被稱爲是一種匹配生產者和消費者的理想工具,一對resume-yield完全一改典型的調用者與被調用者之間的關係。當一個協同程序調用yield時,它不是進入了一個新的函數,而是從一個懸而未決的resume調用中返回。同樣地,對於resume的調用也不會啓動一個新函數,而是從一次yield調用中返回。這項特性正可用於匹配send和receive,這兩者都認爲自己是主動方,對方是被動方。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)
在這種設計中,程序通過調用消費者啓動。當消費者需要一個新值時,它喚醒生產者。生產者返回一個新值後停止運行,並等待消費者的再次喚醒。將這種設計稱爲“消費者驅動”。
可以擴展上述設計,實現“過濾器(filter)”。過濾器是一種位於生產者和消費者之間的處理功能,可用於對數據的一些變換。過濾器既是一個消費者又是一個生產者,它喚醒一個生產者促使其產生新值,然後又將變換後的值傳遞給消費者。

function receive (prod)
loacl 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)
return coroutine.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()))

以協同程序實現迭代器
將循環迭代器視爲“生產者-消費者”模式的一種特例,一個迭代器會產出一些內容,而循環體則會消費這些內容。
例如:寫一個迭代器,使其可以遍歷某個數組的所有排列組合形式。若直接編寫這種迭代器可能不太容易,但若編寫一個遞歸函數來產生所有的排列組合則不會很困難。只要將每個數組元素都依次放到最後一個位置,然後遞歸地生成其餘元素的排列。
function permgen (a, n)
n = n or #a --默認n爲a的大小
if n <= 1 then
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, #a do
io.write(a[i], " ")
end
io.write("\n")
end

調用pergmen:
pergmen({1, 2, 3, 4})
-->2, 3, 4, 1
-->3, 2, 4, 1
-->3, 4, 2, 1
....
-->1, 2, 3, 4

協同程序實現迭代器:
首先將permgen中的printResult改爲yield:
function permgen (a, n)
n = n or #a --默認n爲a的大小
if n <= 1 then
coroutine.yield(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 permutations (a)
local co = coroutine.create(function () permgen(a) end)
return function () --迭代器
local code, res = coroutine.resume(co)
return res
end
end

for p in permutations {1, 2, 3, 4} do
printResult(p)
end

permutations函數使用了一種在Lua中比較常見的模式,就是將一條喚醒協同程序的調用包裝在一個函數中。由於這種模式比較常見,所以Lua專門提供了一個函數coroutine.wrap來完成這個功能。類似於create,wrap創建了一個新的協同程序。但不同的是,wrap並不是返回協同程序本身,而是返回一個函數。每當調用這個函數,即可喚醒一次 協同程序。但這個函數與resume的不同之處在於,它不會返回錯誤代碼。當遇到錯誤時,它會引發錯誤。
function permutations (a)
return coroutine.wrap(function () permgen(a) end)
end

非搶先式的多線程
協同程序提供了一種協作式的多線程。每個協同程序都等於是一個多線程。一對yield-resume可以將執行權在不同線程間切換。然而協同程序與常規的多線程的不同之處在於,協同程序是非搶先式的。當一個協同程序運行時,是無法從外部停止它的。只有當協同程序顯示地要求掛起時(調用yield),它纔會停止。
對於非搶先式的多線程來說,只要有一個線程調用了一個阻塞的操作,這個程序在該操作完成前,都會停止下來。對於大多數應用程序來說,這種行爲是無法接受的。這也導致了許多程序員放棄協同程序,轉而使用傳統的多線程。接下來會用一個有用的方法來解決這個問題。
例如:從網站下載一個文件
require "socket"
host = "www.w3.org"
file = "/TR/REC-html32.html"
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()
同時能夠下載多個文件的例子:
function download (host, file)
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

function receive (connection)
return connect:receive(2^10)
end
在併發的實現中,這個函數在接收數據時絕對不能阻塞。因此,它需要在沒有足夠的可用數據時掛起執行。修改如下:
function receive (connection)
connection:settimeout(0) --非阻塞調用
local s, status, partial = connection:receive(2^10)
if status == "timeout" then
coroutine.yield(connection)
end
return s or partial, status
end

調度程序:
threads = {} --用於記錄所有正在運行的線程
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 --還有線程嗎
if thread[1] == nil then break end --列表是否爲空
i = 1 --重新開始循環
end

local status, res = coroutine.resume(threads[i])
if not res then --線程是否完成任務了
table.remove(threads[i])
else
i = i + 1
end
end
end

main程序如下:
host = "www.w3.org"
get (host, "/TR/html401/html40.txt")
get (host, "/TR/2002/REC-xhtml1-20020801/xhtml.pdf")
...
dispatch() --主循環

一個問題:如果所有線程都沒有數據可讀,調度程序就會執行一個“忙碌等待”,不斷地從一個線程切換到另一個線程,僅僅是爲了檢測是否有數據可讀。
修改此問題:LuaSocket中的select函數,用於等待一組socket的狀態改變,在等待時程序陷入阻塞狀態。
function dispatch ()
--主調度
local i = 1
local connections = {}
while true do
if threads[i] == nil then --還有線程嗎
if thread[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 == #threads then
socket.select(connections) --所有線程都阻塞
end
end
end
end










發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章