[Python] 並行與平行 — 影分身之術!

PC Chen
程式乾貨
Published in
6 min readDec 5, 2020

直接用火影忍者的「影分身之術」來形容 並行Concurrency平行Parallelism 似乎有些不精準,但就需求性而言也很相似了!火影中的影分身之術最大的特性就是可以透過分身累積經驗值,將原本要修練 10 年的忍術,分身出兩人就只要 5 年的時間、分身出四人就只要 2.5 年的時間...以此類推,而平行與並行最大的優點就是要幫助我們節省時間!

旋渦鳴人多重影分身 v.s 君麻呂

假設今天你的程式有些很多流程控制、像是最基本的 for loop,而在這些迴圈裡做的事情重複性很高(EX. 寫入資料、下載東西、洗票XD),我不想要等到第一個迴圈結束後、才跑第二個迴圈,我想要所有迴圈一起跑。這個時候我們就需要用到 並行Concurrency 與 平行Parallelism 的技巧。

先來點背景知識

首先來介紹 進程Process線程Thread 的概念,當我們的電腦執行一個程式時,就會被轉為 Process,我們以 mac 的活動監視器為例,下圖的每個程序都會被轉為一個 PID(Process ID),告訴電腦要把某個應用程式的功能建立起來

Process (進程)

為了完整啟動應用程式的功能,在每一個 Process 之下又會有對應的 Thread(線程、執行緒)來負責某些子功能。例如為了完整啟動「郵件」這個程序,電腦必須有負責收件、寄件、連線信箱等不同的線程來完成,如下圖中我的郵件共有 8 個線程(執行緒)來啟動郵件這個process。

Thread(線程、執行緒)

所以簡單理解,一個 process 裡面可以含有多個 thread,這些底下的 thread 之間可以共享資源、一起合作將該建立好的 process 功能完成。

所以跟 Concurrency、parallelism 有何關係?

並行 Concurrency — multithreading:即在同一個 process 開多個 Thread
平行 Parallelism — multiprocessing:直接多開 process

這兩者的使用情境有些區別,大部分的情況如果你遇到了資料頻繁輸出的 I/O bottleneck,我們選擇使用 multithreading; 如果遇到的是 CPU computation bottleneck,我們選擇使用 multiprocessing。

這樣子想很抽象,我們實際寫個程式來體驗一下,以下我寫了兩個簡單的迴圈,執行後一個會睡5秒、一個會睡10秒,另外也計算了一下執行的時間:

執行與印出的結果很直覺,就大概是 5s+10s=15s 左右會完成

0
1
2
3
4
0
1
2
3
4
5
6
7
8
9
It costs 15.048540830612183 s

可是

這樣子執行似乎很浪費時間,因為程式就是先跑完第一個迴圈、才跑第二個迴圈,有沒有辦法兩個迴圈同時執行呢?
答案當然是有的~就是要使用到我們到現在講這麼多的 Concurrency 與 Parallelism。

並行 Concurrency

就是 multithreading,這時我要把兩個迴圈分配給兩個 thread 同時進行,程式碼如下:

我把兩個迴圈函式分配給 thread_1 與 thread_2 兩個線程同時進行,打印出的結果可以看到在跑第一個迴圈時、第二個迴圈也同時在進行,總共的耗時也就是最長的第二迴圈 10s:

0
0
1
1
2
2
3
3
4
4
5
6
7
8
9
It costs 10.035726070404053 s

平行 parallelism

就是 multiprocessing,跟上面做法有點像,只是我現在把兩個迴圈分配給兩個 process 來同時進行,程式碼如下:

執行結果也發現兩個迴圈會同時進行,耗時也差不多 10s 左右:

0
0
1
1
2
2
3
3
4
4
5
6
7
8
9
It costs 10.035726070404053 s

所以我說...兩個有差嗎?

在上述例子中,過程圖像化大概如以下:

並行(左) v.s 平行(右)

雖然以結果論來看,兩個迴圈執行結果沒有差異,但這是因為這兩個迴圈的任務很單純,就是睡一秒鐘而已。但像我先前提到,如果在迴圈中的任務是要程式寫入資料、下載檔案、或是投票等複雜行為,適當的選擇開分身的方式就很重要了!

情境一

假設現在我在網站爬蟲了 100,000 筆數據,並且要寫入兩個資料庫(測試環境與正式環境),但為了不要搞爛資料庫,我選擇每次500筆塞入資料庫。這時我可以在爬好資料後,寫兩個迴圈、每個都執行 100,000/500=200次 寫入資料庫中。這時我應該用何種方式節省時間呢?

答案是選擇 並行Concurrency-multithreading 的方式比較適當。因為此情境最大的效能 bottleneck 是頻繁地 I/O,而且很重要的一點是:同一個 process 裡面各個 thread 之間是可以共享資源的,也就是每個 thread 可以共享我爬蟲好的數據。因此只要把寫入資料的執行緒分配好,兩個執行緒就可以共同使用要寫入的數據、不用每次都要再重新讀取欲寫入數據。

情境二

現在我開發了一個投票機器人,程式邏輯是模擬人開瀏覽器到投票網站投票後,關掉瀏覽器,如果我想洗個100票,我就跑100次的迴圈、每次都執行我的投票程式。但其實我可以同時開100個瀏覽器(我電腦給力XD),這樣我就等於開了100個分身到投票網站投票!那我該用哪種方式呢?

答案是使用 平行parallelism-multirpocessing 的方式較適當,因為這時遇上的是 CPU computational bottleneck,也就是一個一個開瀏覽器投票其實浪費了許多 CPU 的效能、並沒有把整個 core 吃滿(操爆你的電腦),因此我們可以將 100 個投票程式分給 100 個 process,把你的 CPU 用好用滿!也能夠加快投票的效率!實際程式碼可參考我的 github

延伸閱讀

上述幾個程式碼其實還很粗糙,實作上還有另外一種寫法,是自己定義出 class,並在 class 裡面指定 run 這個方法,這樣會更方便分配 thread 與 process。

不過就一般使用者而言,通常也不會很注重效能,程式能夠執行比較重要XD,所以後續詳細寫法就再看我有沒有時間、或是讀者反應熱烈或許能督促我留下心得XD,目前就先這樣吧~當作工具箱也不錯。

--

--

PC Chen
程式乾貨

喜歡接觸與動手實作各種軟體技術的後端數據工程師 A data- backend engineer who is enthusiastic in learning and implementing any techniques in software engineering.