speed

Concurrency, Asynchronous, Parallelism 是什麼?

前言

在軟體開發的過程中,常會遇到需要提升效能、增加速度的情況,此時想到的解法除了優化程式碼外,常使用的就是 Asynchronous, Concurrency, Parallelism 的技巧吧 。即使不太了解背後的理論,相信大家也天天都在用他們。

但老實說,一半以上的問題都是 Code 寫不好造成的,先好好優化可能比較重要😢

在另一篇文,我們已經介紹了什麼是 Process, Thread, Coroutine ,了解他們分別負責怎樣的工作,有興趣的朋友歡迎參考看看。

延伸閱讀

以下本文會介紹 Asynchronous, Concurrency, Parallelism 各代表什麼。所提到的各種觀念,也會儘量用簡單的白話文來進行介紹,專有名詞的部份會保留英文和加上中文名稱。

Blocking/ Non-blocking/ Synchronous/ Asynchronous

由另一篇文中關於任務的介紹可知,任務的內容,簡單來說可以分成兩種:

  • 中間不可以中斷的
  • 中間可以中斷的

而 Thread 會依照開發者的需求,將任務的返回結果時間點,分成兩種類型:

  • 任務一旦被執行,會等到結果就緒才返回。
    • 此種任務的執行中間不論是否可不可以中斷皆可
  • 任務一旦被執行,不論成功或失敗都立即返回,接著在任務內的工作完成後,會再返回最後結果。
    • 此種任務的執行中間要是可以中斷的

Blocking/ Non-blocking

Blocking 和 Non-blocking 探討的是 Process 上的 Thread 開始執行任務後,收到任務的返回結果前,對 Process/ Thread 的影響。

Thread 的原理是沒收到返回結果就是會一直 Blocking,也就是暫停並等待,不繼續執行下去。在此的返回結果不論是成功 (success) 值或是失敗 (error) 值皆可,但只要有返回結果,就不會 Blocking Thread。而如果某 Process 內是 Single Thread ,該 Thread 被 Blocking 就意味著該 Process 被 Blocking。

因此 Process/ Thread 是否會 Blocking ,重點在返回結果的時間點。

Blocking

任務的類型是:在執行中,沒得到結果前不會返回結果。

因為任務是沒得到結果前不會返回結果的類型,因此任務開始執行後,Thread 會被 blocking,也就是暫停,導致該 Thread 無法繼續執行下去。

而如果該 Process 內是 Single Thread ,該 Thread 被 Blocking 就意味著該 Process 被 Blocking。

Non-blocking

任務的類型是:一旦被執行,就立即返回結果,接著在任務內的工作完成後,會再通知 Thread。

因為任務是會立即返回結果的類型,因此任務開始執行後,Thread 是 Non-blocking,也就是不會暫停,該 Thread 可繼續執行下去。

Synchronous/ Asynchronous

Asynchronous 和 Synchronous 探討的是 Process 上的 Thread 執行各個任務的開始執行時間點。

Synchronous

任務的類型是:在執行中,沒得到結果前不會返回結果。

Process 上的 Tread 內會有多個任務,在執行各任務的過程中,如果需要等待 A 任務完成並返回結果,才能繼續執行 B 任務,則叫做 Synchronous。

Asynchronous

任務的類型是:一旦被執行,就立即返回結果,接著在任務內的工作完成後,會再通知 Thread。

如果不需要等待 A 任務完成並返回結果,就繼續下去執行 B 任務,則叫做 Asynchronous。也就是說 A 任務先開始執行,接著換 B 任務開始執行,但是 A 任務和 B 任務哪個先完成是不一定的。

p.s. Coroutine 是不是完美的符合了 Asynchronous 的特性呢。

小結

別把 Blocking/ Non-blocking 和 Synchronous/ Asynchronous 搞混了~

Blocking 和 Non-blocking 看的是任務是否返回結果了。因此會不會 Blocking 重點在 Thread 是否有拿到任務的返回結果。

Synchronous 和 Asynchronous 是指 Thread 內各任務的開始執行時間點。重點在 Thread 開始執行各任務的時間點,是要求 A 任務開始執行後,等待返回成功,再執行 B 任務,還是不等待 A 任務返回,就繼續執行 B 任務。

延伸閱讀

Concurrency/ Serial/ Parallelism

在另一篇文中介紹 Process/ Thread 時曾經提過,作業系統內同時會有多個 Process 存在,而每個 Process 內可以存在一個到多個 Thread,每個 Thread 內會有一個到多個任務準備要來執行。

另外也提過,一個 CPU 核心一次只能被指派一個 Process,Process 上的 Thread 取得 CPU 時間後才能開始執行任務。而 CPU 從過去的單核心、雙核心、進展到現在的多核心。核心數越多,作業系統一次可以被指派的 Process 就越多,讓電腦有更多工的感覺。

一般來說,Process 被指派給 CPU 核心後,其內的 Thread 可取得 CPU 時間來執行其內的任務,若某 Thread 上的任務無法立即返回結果,造成 Thread Blocking,此時該 Thread 會先讓出 CPU 時間。

以下就來討論,當下的 Thread 讓出 CPU 時間後,Process 之下其他 Thread 之間是怎麼安排執行任務的順序?以及多個 Process 之間是怎麼安排,並指派給 CPU 的?

cpu
cpu

Concurrency

中文是併發。

當下原本在執行任務的 Thread 讓出 CPU 時間後,接著會遇到 3 種適用 Concurrency 的情況:

  1. 該 Thread 所屬的 Process 上只有這個 Thread 或者有多個 Thread ,但也都被 Blocking 中。
  2. 該 Thread 所屬的 Process 尚有其他未被 Blocking 的 Thread
  3. 不論是上述 1 或 2 的情況,有其他優先權更高的 Process 要求 CPU 時間來執行任務。

第 1 種 Concurrency

  • 該 Thread 所屬的 Process 上只有這個 Thread 或者有多個 Thread ,但也都被 Blocking 中。

因為此 Process 上的所有 Thread 都被 Blocking 了,代表該 Process 也處於 Blocking。此時,作業系統內的調度器 (Scheduler) 會立刻將 CPU 讓給其他目前未被 Blocking 的 Process。(Context-Switch 到其他 Process)

p.s. 調度器 (Scheduler) 是用來調度 Process 給 CPU 的。

第 2 種 Concurrency

  • 該 Thread 所屬的 Process 尚有其他未被 Blocking 的 Thread

類似第 1 種情況,此時其他 Thread 會拿走 CPU 時間,要來執行其 Thread 上的任務。至於是哪個 Thread 拿走,主要是比較 Thread 的優先權和任務們進入各 Thread 內的順序。(Context-Switch 到其他 Thread)

因為 Process 分配到的記憶體和變數都可以讓其下所有 Thread 共享,也就是所有 Thread 都可以存取同一個 Process 的記憶體和變數。而每個 Thread 也有自己的 local variable。

Thread 會遇到以下的問題
  1. Race Condition:

多個 Thread 同時存取修改同一個資源時,會產生同步問題 (Synchronization),也就是 Race Condition,此時需要加鎖 (lock) 保護,以控制程式執行的流程。每個鎖同一時間只有一個 Thread 可以擁有他,Thread 擁有鎖的時候,才可以使用 CPU 時間來執行任務。

2. Deadlock:

多個 Thread 分別取得鎖來執行各別的任務,但彼此互相等待任務執行完成才能繼續下去,此時就會產生 Deadlock,使程式無法繼續下去,其實就是種互搶資源造成的問題。

舉例來說,A Thread 有 moveLeft key 在執行任務, B Thread 有 moveRight key 在執行任務。A Thread 同時在等待 moveRight key ,才會將 moveLeft key 釋出,B Thread 同時在等待 moveLeft key ,才會將 moveRight key 釋出,兩個互相等待彼此,形成 Deadlock。

在多執行緒時 ( multi-threading ),常會產生 Race Condition, Deadlock 的問題,維持線程安全 (Thread-Safe) 就是個十分重要的課題。

第 3 種 Concurrency

其他 Process 可能優先權更高或是有搶佔 (preemption) 的情況發生 ,總之就是被中途攔截,都有可能導致 CPU 強行切到其他 Process。

Concurrency 小結

總而言之,Concurrency 提高了 CPU 的效率,不會空等在那邊等任務完成 (time-slicing),相反的,CPU 會在 Thread 或 Process 之間切來切去。

因此,Concurrency 表示任務進入 Thread 內的順序等於開始執行的順序,但不一定等於任務的結束順序。也就是說, Concurrency 代表的是同時處理多個任務。要注意喔!是同時處理多個任務,不是同時執行多個任務,同時執行多個任務的是 Parallelism

multiple processes and threads

多 processes 和多 threads 在作業系統中

延伸閱讀

Serial

中文為串行

就是跟 Concurrency 相反的概念,Concurrency 是遇到 Thread Blocking 時,CPU 就會切給其他 Thread 或是其他 Process 繼續執行其上的任務。

而 Serial 相反,Thread Blocking 的時候,CPU 會繼續在那邊等待,直到任務返回結果。因此,在 Serial 的情況下,任務進入 Thread 內的順序等於開始執行的順序,也等於任務的結束順序。

畢竟任務在 Thread 內就是照順序一個完成再換下一個的。

使用 Serial 的優點在於:任務可以依據想要的順序執行,並且對於共享資源的取用不需要競爭。

要注意的是,Concurrency 是同時處理多個任務,Serial 是一次處理一個任務。

Parallelism

中文是並行。

上面 ConcurrencySerial 探討的是同一個 CPU 上的運作模式,也就是同一個 CPU 在 Thread 或 Process 之間切來切去的執行任務。而 Parallelism 探討的則是多個 CPU 核心上的運作模式。也就是說 Concurrency 描述的與 CPU 的核心數無關,但 parallelism 描述的與多核心及硬體有關。

多個 Process 分別分配到不同 CPU 核心上的,每個 Process 上的 Thread 會同時執行任務,此即為 Parallelism,也就是真正的同時執行多個任務。

當然,各 CPU 核心內一樣可以是 Concurrency 或 Serial 的執行任務,Parallelism 和 Concurrency 是完全不同的概念,但同時也能共存。

而因為是將任務分割成子任務,再分配到不同核心上, 程式的流程和任務的流程就十分重要,必須要達到不論怎樣切割和執行,最後結果都相同。

Concurrency and Parallelism
使用 Parallelism 執行爬蟲程式

要注意的是,在 Concurrency 或 Serial 之下,所有任務的總執行時間不會變短,但 Parallelism 之下,所有任務的總執行時間會變短。

舉例來說,3 個任務,各要執行 5 分鐘,單核心時,在 Concurrency 或 Serial 之下,要 15 分鐘才能全部執行完。相反的,四核心時,在 Parallelism 之下,只要 5 分鐘就會全部執行完。

延伸閱讀

對效能的影響

搭配 Concurrency 和 Parallelism

在另篇文章提過,開發者依照需要,可以在程式中將任務丟入新創造的 Thread 或新創造的 Process 上的 Thread,也就是一個程式可以產生一個到多個 Process,而每個 Process 內也會有一個到多個 Thread 在執行任務。

但,究竟什麼情境下要創造新的 Process,什麼情境下要創造新的 Thread 呢?

舉例來說:

電腦上的很多應用程式,在執行時,一般內部都會有一個 Thread 負責處理所有 UI 繪製相關的任務。假設,要在程式中做一個網路請求,去網路上抓下一些資料,並把資料顯示在 UI 上。

若是把網路請求的任務放在繪製 UI 的 Thread 上執行,就會導致 UI 整個 Blocking,使用者會以為程式當掉,但其實只是在等待網路請求的回應回來。

一般解法是會在程式中產生一個新的 Thread ,將網路請求的任務放到新的 Thread 上執行,這樣 UI 就不會被 Blocking。待網路請求的回應回來後,再把資料給繪製 UI 的 Thread ,畫出新的 UI。

那為什麼這邊是選擇開一個新的 Thread ? 而不是開一個新的 Process,把網路請求的任務丟到其內的 Thread 執行?

原因很簡單,因為同一個 Process 之下記憶體共享,也就是說,網路請求回來的資料,繪製 UI 的 Thread 可直接取到,畢竟兩條 Thread 都在同一個 Proecess 上啊。相反的,如果是將任務放到新的 Process 上,要將資料在 Process 之間搬來搬去就會麻煩的多了。

但 Thread 還是得小心使用,不然就會遇到上面說的 Deadlock 和 Race Condition 的問題

這樣的話,也許有人會說:如果我不需要跟原本的 Process 共享一些資料,那是不是遇到耗費效能的問題就都開新的 Process,把任務丟過去執行呢?還可以避免Deadlock 和 Race Condition 的問題啊!

這個問題沒有對錯,只是有一點要特別注意的,就是產生新的 Process 和新的 Thread 以及在 Process 之間和 Thread 之間做 Context-Switch 都會耗費效能,尤其是創新的 Process 和在 Process 之間做 Context-Switch 耗費的更多。

總之,在程式開發中,遇到耗時比較久的任務時,可以依照以下一些原則,挑選要開新的 Process 或開新的 Thread 來執行:

  • I/O bound 的任務:開新的 Thread 來處理
    • 只要 Concurrency ,在 Thread 之間 Context-Switch ,等待 I/O 的結果回來就好。畢竟 Thread 的 Context-Switch 的效能耗費較少啊
  • CPU bound 的任務:開新的 Process 來處理
    • CPU bound 的任務都是需要大量計算,很吃電腦的效能。開新的 Process 然後搭配多核心,用 Parallelism 的技巧,就可增加效能。

搭配 Coroutine

不論是創新的 Thread 或 Process ,到頭來還是會造成效能損失,畢竟,在Thread 或 Process 之間做 Context-Switch,是作業系統在進行切換的

而 Coroutine 不是!先前提過 Coroutine 說穿了,就是可以隨時暫停執行,再從暫停的地方恢復執行的 function,而暫停和恢復的時間點都是由開發者控制,與作業系統沒有關係,耗費效能也會少很多。

將 I/O 任務發出後,該 Coroutine 可以暫停,先切程式其他地方繼續執行,等到 I/O 結果回來後,該 Coroutine 又恢復繼續執行。

因此在實務上,能使用 Coroutine 處理的情境就會大量的使用 Coroutine 來處理。

在程式開發中,遇到耗時比較久的任務時,原則就可改成:

  • I/O bound 的任務:Coroutine 來處理
    • 開始 I/O 就暫停執行,等待 I/O 的結果回來再恢復執行。
  • CPU bound 的任務:開新的 Process 來處理
    • CPU bound 的任務都是需要大量計算,很吃電腦的效能。開新的 Process 然後搭配多核心,用 Parallelism 的技巧,就可增加效能。

延伸閱讀

結論

Concurrency, Serial, Synchronous, Asynchronous, Blocking, Non-blocking 探討的是程式的結構 (structure),與 CPU 無關。可以想像成程式要寫成怎樣,才能有以上這些特性,是一種 Programming Model。

而 Parallelism 探討的是執行 (execution) 的機制,確實地利用了多核心的優勢,是屬於作業系統面的結構。

在另一篇文,我們已經介紹了什麼是 Process, Thread, Coroutine ,了解他們分別負責怎樣的工作,有興趣的朋友歡迎參考看看。

延伸閱讀

發佈留言