1. 程式人生 > >大前端開發中的執行緒程序問題

大前端開發中的執行緒程序問題

前端開發中的程序執行緒問題(摘錄)

作業系統

在計算機剛剛起步的階段,計算機硬體通常是昂貴的。所以,為了充分發掘計算機的儲存、運算能力,或者邪惡點說是榨乾計算機的硬體資源,使得計算機在儘可能段的時間內處理更多的事情。

交給計算機的任務,大致可以分為兩類:I/O 密集型任務和 CPU 密集型任務。顧名思義,CPU 密集型任務,在執行過程中,需要大量的 CPU 資源。對於這種任務,我們可以大膽地將 CPU 資源交給它來呼叫——反正總是要佔用 CPU 資源的。大體上,涉及到磁碟 I/O、網路存取的任務,就都是 I/O 密集型任務;此類任務往往不需要太多 CPU 資源,對於 CPU 來說,大多數時間被空耗在等待 I/O 完成上了。當人們認識到交給計算機的任務可以分為這兩類的時候,人們就開始考慮如何做 CPU 的任務排程。在任務排程上,人們經歷了多道程式、分時系統與多工系統等階段。

在多工系統中,作業系統接管了所有硬體資源並持有對硬體控制的最高許可權。在作業系統中執行的程式,都以程序的方式執行在更低的許可權中。所有的硬體資源,由作業系統根據程序的優先順序以及程序的執行狀況進行統一的調配。

程序

android中的程序以及生命週期

An unusual and fundamental feature of Android is that an application process’s lifetime is not directly controlled by the application itself. Instead, it is determined by the system through a combination of the parts of the application that the system knows are running, how important these things are to the user, and how much overall memory is available in the system.

android程序優先順序

  1. foreground process
  2. visible process
  3. service process
  4. cached process

    程序間通訊
    多程序的問題:

    1.靜態成員和單例失效:每個程序保持各自的靜態成員和單例,相互獨立。

    2.執行緒同步機制失效:每個程序有自己的執行緒鎖。

    3.SharedPreferences可靠性下降:不支援併發寫,會出現髒資料。

    4.Application多次建立:不同程序跑在不同虛擬機器,每個虛擬機器啟動會建立自己的Application,自定義Application時生命週期會混亂。

多執行緒的優點

  • 將等待 I/O 操作的時間,排程到其他執行緒執行,提高 CPU 利用率
  • 將計算密集型的操作留給工作執行緒,預留執行緒保持與使用者的互動
  • 在多 CPU/多核計算機下,有效吃幹計算能力;
  • 相比多程序的程式,更有效地進行資料共享(在同一個程序空間)

協程

協程的特點在於是一個執行緒執行,那和多執行緒比,協程有何優勢?

最大的優勢就是協程極高的執行效率。因為子程式切換不是執行緒切換,而是由程式自身控制,因此,沒有執行緒切換的開銷,和多執行緒比,執行緒數量越多,協程的效能優勢就越明顯。

第二大優勢就是不需要多執行緒的鎖機制,因為只有一個執行緒,也不存在同時寫變數衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多執行緒高很多。

因為協程是一個執行緒執行,那怎麼利用多核CPU呢?最簡單的方法是多程序+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的效能。

Python對協程的支援還非常有限,用在generator中的yield可以一定程度上實現協程。雖然支援不完全,但已經可以發揮相當大的威力了。

來看例子:

傳統的生產者-消費者模型是一個執行緒寫訊息,一個執行緒取訊息,通過鎖機制控制佇列和等待,但一不小心就可能死鎖。

如果改用協程,生產者生產訊息後,直接通過yield跳轉到消費者開始執行,待消費者執行完畢後,切換回生產者繼續生產,效率極高:

import time

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        time.sleep(1)
        r = '200 OK'

def produce(c):
    c.next()
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()

if __name__=='__main__':
    c = consumer()
    produce(c)

注意到consumer函式是一個generator(生成器),把一個consumer傳入produce後:

首先呼叫c.next()啟動生成器;

然後,一旦生產了東西,通過c.send(n)切換到consumer執行;

consumer通過yield拿到訊息,處理,又通過yield把結果傳回;

produce拿到consumer處理的結果,繼續生產下一條訊息;

produce決定不生產了,通過c.close()關閉consumer,整個過程結束。

整個流程無鎖,由一個執行緒執行,produce和consumer協作完成任務,所以稱為“協程”,而非執行緒的搶佔式多工。

最後套用Donald Knuth的一句話總結協程的特點:

“子程式就是協程的一種特例。”

生成器

生成器是可以迭代的,但是你 只可以讀取它一次 ,因為它並不把所有的值放在記憶體中,它是實時地生成資料:

mygenerator = (x*x for x in range(3))
for i in mygenerator :
… print(i)
0
1
4

看起來除了把 [] 換成 () 外沒什麼不同。但是,你不可以再次使用 for i inmygenerator , 因為生成器只能被迭代一次:先計算出0,然後繼續計算1,然後計算4,一個跟一個的…
yield關鍵字

yield 是一個類似 return 的關鍵字,只是這個函式返回的是個生成器。

def createGenerator() :
… mylist = range(3)
… for i in mylist :
… yield i*i

mygenerator = createGenerator() # create a generator
print(mygenerator) # mygenerator is an object!

for i in mygenerator:
… print(i)
0
1
4

這個例子沒什麼用途,但是它讓你知道,這個函式會返回一大批你只需要讀一次的值.

為了精通 yield ,你必須要理解:當你呼叫這個函式的時候,函式內部的程式碼並不立馬執行 ,這個函式只是返回一個生成器物件,這有點蹊蹺不是嗎。

那麼,函式內的程式碼什麼時候執行呢?當你使用for進行迭代的時候.

現在到了關鍵點了!

第一次迭代中你的函式會執行,從開始到達 yield 關鍵字,然後返回 yield 後的值作為第一次迭代的返回值. 然後,每次執行這個函式都會繼續執行你在函式內部定義的那個迴圈的下一次,再返回那個值,直到沒有可以返回的。

如果生成器內部沒有定義 yield 關鍵字,那麼這個生成器被認為成空的。這種情況可能因為是迴圈進行沒了,或者是沒有滿足 if/else 條件。

Android中的執行緒

single thread mode(not thread-safe)
1. Do not block the UI thread
2. Do not access the Android UI toolkit from outside the UI thread

ios中

1 RAM ROM
  RAM:執行記憶體,不能掉電儲存。ROM:儲存性記憶體,可以掉電儲存,例如記憶體卡、Flash。
由於RAM型別不具備掉電儲存能力(即一掉電資料消失),所以app程式一般存放於ROM中。RAM的訪問速度要遠高於ROM,價格也要高。
2 App程式啟動
App程式啟動,系統會把開啟的那個App程式從Flash或ROM裡面拷貝到記憶體(RAM),然後從記憶體裡面執行程式碼。
另一個原因是CPU不能直接從記憶體卡里面讀取指令(需要Flash驅動等等)。
3 記憶體分割槽:
棧區(stack):存放的區域性變數、先進後出、一旦出了作用域就會被銷燬;函式跳轉地址,現場保護等;
程式設計師不需要管理棧區變數的記憶體;
(棧區地址從高到低分配);
堆區(heap):堆區的記憶體分配使用的是alloc;
需要程式設計師管理記憶體;
ARC的記憶體的管理,是編譯器再便宜的時候自動新增 retain、release、autorelease;
(堆區的地址是從低到高分配)
全域性區/靜態區(static):
包括兩個部分:未初始化過 、初始化過;
也就是說,(全域性區/靜態區)在記憶體中是放在一起的,初始化的全域性變數和靜態變數在一塊區域, 未初始化的全域性變數和未初始化的靜態變數在相鄰的另一塊區域;
eg:int a;未初始化的。int a = 10;已初始化的。
常量區:常量字串就是放在這裡;
程式碼區: 存放App程式碼;
4 注意事項
在iOS中,堆區的記憶體是應用程式共享的,堆中的記憶體分配是系統負責的;
系統使用一個連結串列來維護所有已經分配的記憶體空間(系統僅僅紀錄,並不管理具體的內容);
變數使用結束後,需要釋放記憶體,OC中是根據引用計數==0,就說明沒有任何變數使用該空間,那麼系統將直接收回;
當一個app啟動後,程式碼區,常量區,全域性區大小已固定,因此指向這些區的指標不會產生崩潰性的錯誤。而堆區和棧區是時時刻刻變化的(堆的建立銷燬,棧的彈入彈出),所以當使用一個指標指向這兩個區裡面的記憶體時,一定要注意記憶體是否已經被釋放,否則會產生程式崩潰(也即是野指標報錯)。

ios中的多執行緒

主要由四種:NSThread、NSoperationQueue、NSobject、GCD

Web前端的程序執行緒介紹

瀏覽器多程序架構
跟現在的很多多執行緒瀏覽器不一樣,Chrome瀏覽器使用多個程序來隔離不同的網頁。因此在Chrome中開啟一個網頁相當於起了一個程序

那麼Chrome為什麼要使用多程序架構?
在瀏覽器剛被設計出來的時候,那時的網頁非常的簡單,每個網頁的資源佔有率是非常低的,因此一個程序處理多個網頁時可行的。然後在今天,大量網頁變得日益複雜。把所有網頁都放進一個程序的瀏覽器面臨在健壯性,響應速度,安全性方面的挑戰。因為如果瀏覽器中的一個tab網頁崩潰的話,將會導致其他被開啟的網頁應用。另外相對於執行緒,程序之間是不共享資源和地址空間的,所以不會存在太多的安全問題,而由於多個執行緒共享著相同的地址空間和資源,所以會存線上程之間有可能會惡意修改或者獲取非授權資料等複雜的安全問題。

在瞭解這個知識點線,我們需要先說明下什麼是瀏覽器核心。

瀏覽器核心
簡單來說瀏覽器核心是通過取得頁面內容、整理資訊(應用CSS)、計算和組合最終輸出視覺化的影象結果,通常也被稱為渲染引擎。從上面我們可以知道,Chrome瀏覽器為每個tab頁面單獨啟用程序,因此每個tab網頁都有由其獨立的渲染引擎例項。

瀏覽器核心是多執行緒
瀏覽器核心是多執行緒,在核心控制下各執行緒相互配合以保持同步,一個瀏覽器通常由以下常駐執行緒組成:

  1. GUI 渲染執行緒
  2. JavaScript引擎執行緒
  3. 定時觸發器執行緒
  4. 事件觸發執行緒
  5. 非同步http請求執行緒
  6. GUI渲染執行緒

GUI渲染執行緒負責渲染瀏覽器介面HTML元素,當介面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該執行緒就會執行。在Javascript引擎執行指令碼期間,GUI渲染執行緒都是處於掛起狀態的,也就是說被”凍結”了.

Javascript引擎執行緒
Javascript引擎,也可以稱為JS核心,主要負責處理Javascript指令碼程式,例如V8引擎。Javascript引擎執行緒理所當然是負責解析Javascript指令碼,執行程式碼。

Javascript是單執行緒的
Javascript是單執行緒的, 那麼為什麼Javascript要是單執行緒的?

這是因為Javascript這門指令碼語言誕生的使命所致:JavaScript為處理頁面中使用者的互動,以及操作DOM樹、CSS樣式樹來給使用者呈現一份動態而豐富的互動體驗和伺服器邏輯的互動處理。如果JavaScript是多執行緒的方式來操作這些UI DOM,則可能出現UI操作的衝突; 如果Javascript是多執行緒的話,在多執行緒的互動下,處於UI中的DOM節點就可能成為一個臨界資源,假設存在兩個執行緒同時操作一個DOM,一個負責修改一個負責刪除,那麼這個時候就需要瀏覽器來裁決如何生效哪個執行緒的執行結果。當然我們可以通過鎖來解決上面的問題。但為了避免因為引入了鎖而帶來更大的複雜性,Javascript在最初就選擇了單執行緒執行。

GUI 渲染執行緒 與 JavaScript引擎執行緒互斥!
由於JavaScript是可操縱DOM的,如果在修改這些元素屬性同時渲染介面(即JavaScript執行緒和UI執行緒同時執行),那麼渲染執行緒前後獲得的元素資料就可能不一致了。因此為了防止渲染出現不可預期的結果,瀏覽器設定GUI渲染執行緒與JavaScript引擎為互斥的關係,當JavaScript引擎執行時GUI執行緒會被掛起,GUI更新會被儲存在一個佇列中等到引擎執行緒空閒時立即被執行。

JS阻塞頁面載入
從上面我們可以推理出,由於GUI渲染執行緒與JavaScript執行執行緒是互斥的關係,當瀏覽器在執行JavaScript程式的時候,GUI渲染執行緒會被儲存在一個佇列中,直到JS程式執行完成,才會接著執行。因此如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染載入阻塞的感覺。

定時觸發器執行緒
瀏覽器定時計數器並不是由JavaScript引擎計數的, 因為JavaScript引擎是單執行緒的, 如果處於阻塞執行緒狀態就會影響記計時的準確, 因此通過單獨執行緒來計時並觸發定時是更為合理的方案。

事件觸發執行緒
當一個事件被觸發時該執行緒會把事件新增到待處理佇列的隊尾,等待JS引擎的處理。這些事件可以是當前執行的程式碼塊如定時任務、也可來自瀏覽器核心的其他執行緒如滑鼠點選、AJAX非同步請求等,但由於JS的單執行緒關係所有這些事件都得排隊等待JS引擎處理。

非同步http請求執行緒
在XMLHttpRequest在連線後是通過瀏覽器新開一個執行緒請求, 將檢測到狀態變更時,如果設定有回撥函式,非同步執行緒就產生狀態變更事件放到 JavaScript引擎的處理佇列中等待處理