iT邦幫忙

6

Week3 - 你有沒有想過,到底Server是如何「同時處理多個requests」的? - 行程、線程、協程篇 [NodeJs轉Golang的爆炸之旅系列]

文章也同時發表於medium(`・ω・´)”

稍微提一下,以下所有圖畫都是我妹妹幫忙畫的,希望有幫助大家~


髒沙發LineBot在開發時曾經碰過一個問題,就是在處理大圖的時候,有兩個步驟,分別是

  • 圖片處理:屬於CPU操作
  • 圖片儲存:屬於IO操作

而在上篇文章有介紹Node.js對於IO操作是不會阻塞的,CPU操作是會阻塞的,這是由於Node.js是single thread,沒有multi thread的幫忙來處理CPU-bound。

在Golang方面,由於goroutine擁有操作多個thread的能力,所以可以讓每個thread來分工CPU操作。

Golang處理的方法遵照MPG模型來處理這類的行為,但如果直接討論可人會讓人一臉矇逼。我們可以介紹「行程到現在高併發的協程」來慢慢了解「出現什麼問題,可用什麼方法應對」,這樣應該會比較好了解。

行程、線程、協程

我們可先有一個CPU核心運作的概念,以單純的方式來討論問題,「單一個CPU核心,多執行緒」的運作方式如下

單CPU核心會切成許多的時間切片(timeslicing),一下做thread A的事情,一下做thread B事情,而當做得非常快,就像用來「一個人執行多件事」,舉個最近很好看的音速小子來當例子

單CPU核心就是音速小子,因為做事太快,所以同時做切菜、拖地、與蛋頭博士泡茶,在一般人的感知裡就像同時做這三件事情

有了這個概念後,我們就可以開始討論

行程

行程是「資源獨立運行的最小單位」,而在以前,因為還沒有「multi thread的概念」,所以行程同時也是「執行的最小單位」,這導致一個問題

在切換thread A或thread B的事情時,開銷變得很大

我們稱這個切換行為為context switch,當音速小子切菜時身上需要有菜刀、大蔥、胡蘿波,之後拖地又要準備拖把、水桶,再來要泡茶還要準備茶杯,在這每次的切換工作時也要「切換資源」,導致切換的成本變很大,那這要怎麼解決呢?

於是就有了線程的概念。

線程

線程這個概念的出現取代了行程「執行的最小單位」的位置,所以要處理多件事情時,可以不需開多個行程,而是「一個行程多個線程」,即

音速小子把這三件事情所需的各個資源都帶在身上,這樣切換工作時就不用再拿菜刀換拖把了

協程

一切都很美好,直到「周邊IO-瓦斯爐」滾茶讓音速小子乾等了許久。

由於時間切片是固定的,每次做事的時間也是固定的,這導致音速小子在切換到泡茶這件事時,一直在等周邊的瓦斯爐把茶滾好,所以一直看著茶發呆,這樣等於白白浪費了這些時間

是否能在滾茶時,讓thread的事情從滾茶換成擦茶桌呢?

可以,這時我們可以在程式碼這樣寫

go 滾茶() //這行不會等他完成即會往下一行跑
擦完茶桌後,再等茶滾好()

這樣音速小子就可以更充分利用時間。

你可能有注意到,我們在程式碼裡面做了跟「單一個CPU核心,多執行緒」類似的行為,即是「切換事情」。

值得注意的是,

協程的切換是由程式碼完成的,而單一個CPU核心線程的切換是由時間切片固定分配的。

這代表協程更加的輕量,因為比起線程它切換的開銷更小了,因為它是在原thread裡面做切換,不像多線程還要跨thread來切換。

多核心與單核心的差異

大家會發現,單核心始終是一個人,所以很快速切換處理事情的時間,其實同等於認真處理一件一件事情的時間

甚至在快速切換時,因為多了這個切換的開銷,往往會比認真一件一件處理來得更久

所以這時就出現了Parallel(平行)這個概念,即是多核心,大家可以看到由於核心的增加可以讓同一時間可以處理更多thread

有了這些概念後,歡迎來到現代世界,我們把多核心納入討論

多核心的世界裡,需要考慮的事情比單核心來得更多,這些核心要怎麼分配thread就變成了一門學問,大家要先有一個觀念,就是thread分別有

  • kernel thread:系統層級的thread,由核心所支持,一般來說一個核心支持一個kernal thread,可操作各種底層API與接受user thread所要求要做的事情
  • user thread:使用者層級的thread,無法直接調用底層API,都要透過kernal thread來進行調度

現在有查爾斯與音速小子兩個核心,現在他們一樣要做切菜、拖地、與蛋頭博士泡茶這三件事情,他們一樣非常快速的做這些事,但三件事情給兩個人做勢必會遇到「分配」上的問題,所以有以下方法來分配事情。

講得有點抽象,以下配合幾個角色,我們用圖來解釋

  • K1: 查爾斯,是kernel thread 1
  • K2: 音速小子,是kernel thread 2
  • U1: 切菜的事情,是user thread 1
  • U2: 拖地的事情,是user thread 2

一對一:大家就好好做自己的事

  • 優點:
    • 一個kernal thread就對上一件事情,意義上來說實現了真正的平行處理
    • 當事情阻塞了其中一個kernal thread,並不會導致其他kernal thread阻塞。
  • 缺點:
    • 由於一般來說一個核心支援一個kernal thread,所以三件事情事實上要再新增一個核心才可以一次處理三件
    • 當在切換事情的時候,由於是kernel thread在切換,所以開銷是很大的。

一對多:把全部事情都塞給一個人做

  • 優點:
    • 與一對一不同,由於事情是在使用者層級切換,統一由一個kernal thread處理,所以並不會被核心數限制。
    • 與一對一不同,由於事情是在使用者層級切換,不用切換kernal thread,所以相對來說快很多。
  • 缺點:
    • 如果一件事贏卡住了kernal thread,那所有事情都會被卡住。
    • 增加核心數對於系統速度幾乎沒有幫助,因為所有事情都交給一個人做了。

多對多:等等,全部事情大家分配著做才對吧!

  • 優點:
    • 擁有一對一與一對多的全部優點
  • 缺點:
    • 系統變得複雜,導致user thread較難區分哪些事情是重要的,該分配給哪個kernal thread

做個統整

大家可以發現,原本的行程為什麼後來需要線程、內核線程、用戶線程、協程,其實最大的目標不外乎就是

  • 讓核心在切換事情的成本下降
  • 降低核心等待的情形發生,即善用核心的所有時間

所以統整下來,可以將行程、線程、協程的關係畫張廣義的示意圖

簡單的來說就是依照不同的情境,以不同的層次來分配他們事情

  • 需換人也需換資源:那就換行程
  • 需換人但不需換資源:那就換線程
  • 不需換人也不需換資源,但不要乾等,先做其他事:那就用協程

稍微介紹完這些概念後,接下來會介紹Golang的MPG模型,謝謝你的閱讀ヽ(・ω・ゞ)

如有錯誤歡迎勘正指教,謝謝你的閱讀~


2
JackKuo
iT邦新手 4 級 ‧ 2020-03-02 05:43:23

台灣用法應是行程(process)、執行緒(thread)

感謝~
已修改名稱,關於翻譯的名詞要使用哪個,其實我思考很久,
一個英文單字有很多種翻法,
所以文章中如果有提到的專業名詞,我會盡量把英文名詞一起寫上,
希望能更明確表達意思◝(・ω・)◟

JackKuo iT邦新手 4 級 ‧ 2020-03-03 20:37:35 檢舉

可以參考微軟的對照表:https://www.microsoft.com/en-us/language

2
dragonH
iT邦超人 5 級 ‧ 2020-03-04 10:08:23

Node.js對於IO操作是不會阻塞的,CPU操作是會阻塞的,這是由於Node.js是single thread,沒有multi thread的幫忙來處理CPU-bound。

nodejs 已經有 worker thread 了唷

以前也有 child_processcluster 可以用

參考

你好~
感謝你的回覆,
坦白說我一直在思考要不要把work thread, child_process, cluster納入,
因為work thread是新的特性,
child_process, cluster又稍稍是比較特別的做法,
怕寫入文章會造成主題發散,但看到你的留言後又覺得如果不寫進去,
也可能會造成觀念上的錯誤,
所以,我之後會把這些概念評估在文章裡,再次感謝你的回覆~

dragonH iT邦超人 5 級 ‧ 2020-03-05 15:47:16 檢舉

我是覺得稍微提一下就好

畢竟你的主題是 Go

我會提這個是因為每個在跟 nodejs 做比較的文章

幾乎都是以我引用你的那段話來做起點

很多人看完就覺得 nodejs 就是完全拿 CPU-intensive 的 task

一點辦法都沒有

/images/emoticon/emoticon12.gif

1
eric19740521
iT邦新手 3 級 ‧ 2020-08-10 03:25:08

行程、線程、協程

大陸講法為進程/線程/協程

是的你說的沒錯,或許以後都用英文來說明這些名詞會比較單純些XD

中英文對照.謝謝...
你寫的文章很棒.謝謝

我要留言

立即登入留言