前言
雖然不經常用到協(xié)程,但是也不能談虎色變。同時,在有些場景,協(xié)程會起到一種不可比擬的作用。所以,了解它,對于一些功能,也會有獨特的思路和想法。
協(xié)程
概念
關于進程和線程的概念就不多說。
那么從多線程的角度來看,協(xié)程和線程有點類似:擁有自己的棧,局部變量和指令指針,又和其他協(xié)程共享全局變量等一切資源。
主要的區(qū)別在:一個多線程程序可以并行運行多個線程,而協(xié)程卻要彼此協(xié)作運行。
什么是協(xié)作運行?
也就是任意指定的時刻,只能有一個協(xié)程運行。
很懵對不對?
層級調用和中斷調用
所有的語言中,都存在層級調用,比如A調用了B,B在執(zhí)行過程中又去調用C,C執(zhí)行之后,返回到B,B執(zhí)行完畢之后,返回到A,最后A執(zhí)行完畢。
這個過程就像棧一樣,先進后出,依次從棧頂執(zhí)行。所以,它也叫調用棧。
層級調用的方式是通過棧來實現的。
中斷調用,就是我在A函數中可以中斷去調用B函數,函數B中可以中斷去調用A。
比如
function A()
print(1)
print(2)
print(3)
end
function B()
print("a")
print("b")
print("c")
end
那么如果是中斷調用,就可能輸出: 1 a b 2 3
協(xié)程的優(yōu)勢
其實一句話總結。
拿著多線程百分之一的價錢,干著多線程的事情,還效率賊高。
這樣的員工誰不喜歡?
這也是Go語言高并發(fā)的原因之一。
怎么理解協(xié)程?
左手畫圓,右手畫方,兩個手同時操作,這個叫并行,線程就是干這個事情的。
左手畫一筆,切換到右手畫一筆,來回交替,最后完成,這叫并發(fā),協(xié)程就是為了并發(fā)而生。
那么線程不能完成協(xié)程的事情?
舉個例子,一個作業(yè),可以交給兩個人完成,這叫并行。創(chuàng)建兩個線程就可以了。
那么其中一個人在做事情的時候,突然有人要插入一個比較緊急的事情,這個人就必須停下手中的事情,去處理那個緊急的事情。停下叫阻塞。線程本身是支持阻塞的。
但是這里就有一個問題,不管是創(chuàng)建線程還是切換線程,所帶來的成本遠遠大于用阻塞的方式實現并發(fā),要考慮兩個線程之間的數據同步,加鎖解鎖,臨界問題等等。而協(xié)程并不需要來回切換。所以,并發(fā)的線程越多,使用協(xié)程來代替的性能優(yōu)勢就越明顯。
所以,可以知道協(xié)程不是線程,它的資源是共享的,不需要如同多線程一樣加鎖來避免讀寫沖突。
比如創(chuàng)建一個線程棧需要1M,那么協(xié)程棧只需要幾K,或者幾十K。
協(xié)程的缺點
協(xié)程本質上是單線程,所以它吃不到多核CPU的福利,需要與進程配合才能辦到。
然后協(xié)程也不是那么好控制,需要寫一些代碼進行手動控制它的中斷調用。
Lua的協(xié)程
Lua的協(xié)程是非對稱協(xié)程。
簡單來說,非對稱就是需要兩個函數來控制協(xié)程的執(zhí)行。
Go的協(xié)程是對稱協(xié)程,有興趣可以去了解下。
再說簡單一點就是,非對稱協(xié)程,需要yield函數來掛起,resum函數來恢復,同時,哪里掛起,就恢復到哪里去。
對稱協(xié)程,只需要yield一個操作。
簡單的例子
理解完這個,就很容易理解Lua的協(xié)程代碼。即在哪里yield,下次調用resum,就恢復到y(tǒng)ield那里,繼續(xù)執(zhí)行。
local co = coroutine.create(function()
print(1)
coroutine.yield()
print(2)
coroutine.yield()
print(3)
end)
coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)
第一次resume,就是執(zhí)行協(xié)程函數co ,print(1)
第二次resume,就是print(2)
第三次resume,就是print(3)
四種狀態(tài)
一個協(xié)程有四種狀態(tài):
- 掛起(suspended)
- 運行(running)
- 正常(normal)
- 死亡(dead)
可以通過函數**coroutine.status(co)**來進行檢查協(xié)程的狀態(tài)。
當一個協(xié)程創(chuàng)建的時候,它處于一個掛起狀態(tài),也就是說,協(xié)程被創(chuàng)建,不會自動運行,需要調用函數**coroutine.resume(co)**用來啟動或者再啟動一個協(xié)程的執(zhí)行,將掛起狀態(tài)改成運行狀態(tài)。
當協(xié)程中執(zhí)行到最后的時候,整個協(xié)程就停止了,變成了死亡狀態(tài)。
協(xié)程報錯
由于協(xié)程resume在保護模式下,所有錯誤都會返回給它,即哪怕協(xié)程處于死亡狀態(tài),調用coroutine.resume(co),也不會出現任何錯誤。
同時協(xié)程中的錯誤不容易被發(fā)現,所以需要使用xpcall來進行拋出。
lua5.1需要封裝一層來達到這個目的
--協(xié)程,Lua5.1無法掛C函數,這樣進行處理,協(xié)程中出問題,會拋出錯誤
local coroutCanTrowError = function()
xpcall(showGetSpecialCollect,__G__TRACKBACK__)
end
self.m_showGetSpecialRune = coroutine.create(coroutCanTrowError)
coroutine.resume(self.m_showGetSpecialRune)
xpcall函數,異常處理函數,是lua強大的異常處理函數,這個以后做分享。
交換數據
lua的協(xié)程主要是通過resume-yield來進行數據交換的。
即第一個resume函數會把所有的額外參數傳遞給協(xié)程的主函數。
什么意思呢?
local co = coroutine.create(function(a,b,c)
print("co",a,b,c)
end)
coroutine.resume(co,1,2,3)
resum函數會把參數都傳遞給協(xié)程的主函數。所以這里輸出co 1 2 3
local co = coroutine.create(function(a,b,c)
print("co",a,b,c)
coroutine.yield(a + b,a - b , c + 2)
end)
print(coroutine.resume(co,1,2,3))
輸出就是
co 1 2 3
true 3 -1 5
在yield的時候,會把參數都返回給resume,這里有點拗口。
可以這么理解,yield中的參數就是resum的返回值。當然這里要注意的是,resume的返回值第一個是resume是否成功的標志。
這種類似于
local co = coroutine.create(function(a,b,c)
return a - b
end)
print(coroutine.resume(co,1,2,3))
輸出 true,-1
也就是協(xié)程主函數的返回值都會變成resume的返回值。
是不是覺得有無限可能了?
但是,值得注意的是,雖然這種機制會帶來很大的靈活性,但是,使用不好,可能會導致代碼的可讀性降低。
著名的生產者和消費者
這是協(xié)程最經典的例子。
即一個生產函數(從文件讀數據),一個消費函數(將讀出來的值寫入另一個文件)。
function producer()
while true do
local x = 1
send(x)
end
end
function consumer()
while true do
local x = receive()
print(x)
end
end
local list = {}
function send(x)
table.insert(list,x)
end
function receive()
if #list > 0 then
local a = table.remove(list)
return a
end
end
producer()
consumer()
會發(fā)生什么?
當然,也可以將兩個放到不同的線程中去處理,但是這樣對于數據量大的時候來說就是個災難。
協(xié)程怎么去實現?感興趣的可以去了解下。
function eceive (prod)
local status, value = coroutine esume(prod)
return value
end
function send (x)
cooutine.yield(x)
end
function poducer()
return coroutine.c eate(function ()
while true do
local x = io.read ()
send (x)
end
end)
end
function filter(prod)
return coroutine.ceate(func ()
for line= 1, math.huge do
local x = receive (prod)
x = string.format(%s ”, line, x)
send(x)
end
end )
end
function conrumer(prod)
while true do
local x = receivee(prod)
io. write (x ,”\n”)
end
end
conrumer(filter(poducer()))
不用擔心性能問題,因為是協(xié)程,所以任務開銷很小,基本上消費者和生產者是攜手同行。
應用場景
首先,再重復一遍,協(xié)程是并發(fā),不是并行。
有個需求,和我們相關的。
棋盤有4種bonus,停輪之后,每個bouns的效果不一樣,比如翻轉,比如收集等等,效果執(zhí)行完畢之后,再進行下一個,直到結束。
這個是很簡單的需求,可以用for循環(huán)執(zhí)行。
如果加上,bonus在執(zhí)行效果中,會有部分等待,或者延遲效果?那么for循環(huán)就不能滿足,因為在延遲的時候,函數就返回,執(zhí)行下一個for循環(huán)了。
再比如加上,bonus在執(zhí)行效果中,會牽涉到另外一堆邏輯。
等等。
然后,有人會說,遞歸也可以實現。
但是,首先得明白一點,遞歸是個思想,而協(xié)程是個機制。兩個本質上不是一個東西,更何況,遞歸會涉及到其他東西。這個等下會說。
遞歸
遞歸的含義是:在調用一個函數的過程中,直接或者間接調用了函數本身。
一個很簡單的遞歸就是:
local a = nil
a = function(n)
if n == 1 then
return 1
end
print("a "..n)
n = n * a(n - 1)
print("b "..n)
return n
end
print(a(5))
請問輸出什么。
輸出
a 5
a 4
a 3
a 2
b 2
b 6
b 24
b 120
120
再來溫習下遞歸的特點:
- 原來的基礎上不斷“向下/向內”開辟新的內存空間。(即每一次的調用都會增加一層棧,每當函數返回的時候,就減少一層棧。)所以,對于遞歸來說,遞歸的層次越多,很容易導致棧溢出。這也決定了遞歸本身的效率不高。
- 遞歸是暫停阻塞,什么是暫停阻塞呢?也就是遞歸調用函數的以下部分是不會被執(zhí)行的,只有返回之后才能執(zhí)行。
說到這里,不得不說lua這個語言有一個非常不錯的優(yōu)化——尾調用。
尾調用
什么是尾調用呢?
一個函數返回您一個函數的返回值。
是不是有點拗口。
我們看下代碼。
function A(x)
return B(x)
end
通俗的來說,就是當一個函數調用是另一個函數的最后一個動作的時候,該調用才能算上尾調用。
上面例子中,A調用完B之后,就沒有任何邏輯了。這個時候,Lua程序不需要返回函數A所有在得函數,那么程序自然而然就不需要保存任何有關于函數A的棧(stack)信息。
該特性叫“尾調用消除”。
別小看這個特性。
通常,函數在調用的時候,會在內存中形成一個“調用記錄”,它保存了調用位置和內部變量等信息。
例如
function A(x)
local a = 1
local b = B(x)
local c = 2
return a + b + c
end
在程序運行到調用函數B的時候,會在A的調用記錄上方,形成B的調用記錄,等到B返回之后,B的調用記錄才會消失。那么調用的函數越多,就如同棧一樣,依次放到A、B等等的上面,所有的調用記錄就形成了調用棧。
那么想象一下,函數A調用B,B調用C,C調用D…會發(fā)生什么。
是棧溢出。
棧和堆不一樣,棧是系統(tǒng)分配的,也可以說棧是系統(tǒng)預設的空間,堆是自己申請的。所以,當我們的函數調用層次太深,導致保存調用記錄的空間大于了系統(tǒng)分配的棧,那么就會導致棧溢出。然后各種莫名其妙的Bug就出現了,比如遞歸不返回了;比如調用函數不返回了等等。
這個時候,Lua的尾調用消除就起到了關鍵性作用。它是函數最后一步操作,所以不需要保存外層函數的調用記錄,它里面的所有內部變量等信息都不會再用到了,所以就不需要??臻g去保存這些信息。
那么,可能會有人說,那么我在函數尾部調用另一個函數不就可以了么?
并不是。
function A(x)
return 1 + B(x)
end
function C(x)
return true and D(x)
end
上面的兩個函數就不是尾調用。
函數A中,最后一步不是B函數,而是+運算符
函數C中,最后一步不是D,而是and 運算符
function A(x)
if x > 0 then
return B(x)
end
return C(x)
end
這樣的函數B和函數C才是尾調用。
可能這樣覺得沒啥,我們做個實驗。
function A(x)
return 1 + B(x)
end
function B(x)
return x * 2
end
print(collectgarbage("count"))
for i = 1,1000000 do
--print(A(i))
A(i)
end
print(collectgarbage("count"))
輸出
相差:0.09960
那么看下尾調用
function A(x)
return B(x,1)
end
function B(x,y)
return x * 2 + y
end
print(collectgarbage("count"))
for i = 1,1000000 do
--print(A(i))
A(i)
end
print(collectgarbage("count"))
相差是0.035
接近3倍。
可能有人會說,這點性能應該沒啥吧。我想說的是,這里只是一個簡單的計算,本來保存的數據都不大,如果是實際開發(fā)中,需要保存的東西更大了。
尾遞歸
理解了尾調用,那么我們來看看尾遞歸
前面也說了,遞歸依賴棧,非常消耗內存。還有,別以為有些功能就遞歸幾次,就沒有什么性能消耗。那誰又能保證在遞歸的函數中有大量的其他函數調用或者數據處理呢?
畢竟有時候很容易寫出,遞歸函數中調用其他函數,其他函數又調用一堆其他函數這種套娃的代碼。
例如,最開始的代碼
local a = nil
a = function(n)
if n == 1 then
return 1
end
return n * a(n - 1)
end
a(5)
這就是一個“不合格”的遞歸函數。
那么尾遞歸怎么寫呢?
local a = nil
a = function(n,m)
if n == 1 then
return m
end
return a(n - 1,n * m)
end
a(5,1)
回調
通常會拿協(xié)程同回調也就是callback比較。因為兩者都可以實現異步通信。
比如:
bob.walkto(jane)
bob.say("hello")
jane.say("hello")
當然,不可能這樣運行,那么會導致一起出現。所以有下面的方式。
bob.walto(function ( )
bob.say(function ( )
jane.say("hello")
end,"hello")
end, jane)
如果再多一些呢?
再結合上面說的調用記錄的說法,可能層次深了,發(fā)現咋不回調了。文章來源:http://www.zghlxwxcb.cn/news/detail-692957.html
如果用協(xié)程來實現就是:文章來源地址http://www.zghlxwxcb.cn/news/detail-692957.html
function runAsyncFunc( func, ... )
local current = coroutine.running
func(function ( )
coroutine.resume(current)
end, ...)
coroutine.yield()
end
coroutine.create(function ( )
runAsyncFunc(bob.walkto, jane)
runAsyncFunc(bob.say, "hello")
jane.say("hello")
end)
coroutine.resume(co)
到了這里,關于淺談Lua協(xié)程和函數的尾調用的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!