這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
即使我死了,埋在土地裡,我也要用我腐爛的聲帶喊出:閑置CPU是可恥。——孔子
進程,線程?並行,並發?
由於單核CPU效能過剩,而如此高的效能卻只能運行一個程式無疑是極大的浪費,因此多任務作業系統應運而生。
作業系統把每個任務映射為一個進程,通過CPU輪詢讓每個進程輪流程執行,從而讓人誤以為所有進程都在同時運行,實際上再同一時刻只有一個進程運行,這叫並發。當CPU有多核或者多線程的時候,在同一時刻就會同時有多個進程運行,這叫並行。
隨著時代的進步,越來越多的商務邏輯需要高並發,需要高效能。因為建立進程,切換進程,進程間通訊,成本之高,使得多個進程協同運行並不能滿足需求,於是多線程應運而生。通過在每個進程裡建立多個線程,這些線程共用一個進程資料,這幾乎彌補了多進程的所有缺點。當今主流作業系統都是多線程作業系統,例如:windows,linux,線程為作業系統最小調度單位,通過調度器,為每一個線程分配時間片。
隨著硬體效能的提升,隨著時間的考驗,多進程的缺點變成了多線程的缺點:建立線程,切換線程,線程間通訊,成本之高。因為單核CPU效能已經足夠,很多時候需要的是並發,而不是並行。而進程和線程都是由系統接管,並發還是並行都由系統說了算。因此人們渴望只並發的多線程,從而協程應運而生。
什麼是協程?
設想一下:
多個線程處理各自的隊列,當隊列有任務時,線程依次執行,當隊列為空白時,線程休眠,如此迴圈。
如果線程數量上千甚至上萬,這些線程同一時刻只有少部分隊列有任務,那麼作業系統會頻繁的掛起線程,啟用線程,並且所有線程都會佔用系統資源,這會導致系統不堪重負,即使只有少數線程運行,也會讓系統變得緩慢,甚至每個線程都需要鎖,雖然有許多無鎖演算法,但這會讓編程難度從普通上升到地獄,而操作線程,鎖的時間也許遠遠大於線程執行任務的時間。
如果我們可以擁有自己的線程調度器,可以在應用程式層隨意調度線程,避免線程進入核心狀態,避免線程爭奪資源,降低線程切換開銷,可以在單核CPU高並發榨乾效能,這樣的多線程就是協程。
如果用協程來完成上述任務,可以解決上述的所有缺點。
由於調度器太底層,幾乎把整個語言固定了,因此只有那些為了高並發而生的語言,才會有調度器,例如:GoLang,ErLang等,這些語言適合編寫高並發程式。對於那些不是為了高並發而生的語言,它們也需要協程,但是它們不需要調度器,它們只要手動調度,這就已經足夠了。
用協程取代非同步回調
Lua 有協程,沒有調度器,即使這樣,協程依舊是Lua最強特性沒有之一,這一特性經常被生手忽略,因為他們根本想不到該怎樣使用協程。
function f() f1() f2() f3()end
上述代碼邏輯很清晰,f()函數按順序執行了f1(),f2(),f3()出於某些原因,也許動畫延遲,也許網路請求,也許有意延遲,f1(),f2()不能馬上得到結果,同時,也不能讓線程卡在這等著。因此,這裡就變成了非同步需求,代碼可能變成下面這樣:
function f() f1(function() f2(f3) end)end
以上邏輯看起來很依舊很清晰,f1(),f2(),都接受一個函數作為非同步回調,但是在f1()傳參時,明顯出現了不和諧代碼,因為f3()要作為f2()的參數,需要用function() end產生一個閉包傳遞。但是,一旦需要執行更多的函數,代碼將慘不忍睹:
function f() f1(function() f2(function() f3(function() f4(f5) end) end) end)end
回到前一段代碼:
function f() f1(function() f2(f3) end)end
這段代碼非同步回呼函數只有3個,看似邏輯清晰,實際上存在重大隱患,從f()函數體來看,只能看出f1(),f2()分別接受一個函數作為參數,並不能明確f1(),f2(),f3() 是否按順序調用(也許內部壓根就沒有碰這個參數), 因而使得f()邏輯很不清晰,甚至連內部執行順序都不能保障。如果存在大量非同步回調,邏輯混亂程度更是不敢想象,俗稱:回調地獄。
有沒有可能讓非同步呼叫,也可以像同步調用那樣寫呢?用協程可以做到。
local function async(handler) local runn = coroutine.running() handler(function() coroutine.resume(runn) end) coroutine.yield()endfunction f() local co = coroutine.create(function() async(f1) async(f2) async(f3) ... async(fN) end) coroutine.resume(co)end
f()內部將f1(),f2(),...,fN()按順序非同步呼叫,這段代碼看起來是同步的,但它的的確確是非同步。協程的確可以優雅解決回調地獄問題,當然,這個寫法並不固定,可根據具體需求進行編碼。
上述代碼原理是:
用協程去執行第一個非同步函數,同時跳回主程式,因為協程跟主程式在同一線程,因此,在協程裡調用跟在主程式調用是一樣的,當非同步呼叫完成再跳回協程,繼續下一個非同步呼叫,如此迴圈。
非同步回調並非沒有優勢。比如:可以把簡單問題搞得複雜,把複雜問題搞出很多問題,讓開發人員每天都過得充實。