iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 14
2
自我挑戰組

30天學python系列 第 14

[Day14] 行程 (process) 和線程 (thread)

  • 分享至 

  • xImage
  •  

多行程 (Multi-Process) 還是多線程 (Multithreading)

作業系統在切換行程 (process) 或線程 (thread) 時需要先保存當前執行的環境,把新任務的執行環境準備好,才能開始執行。切換過程雖然很快,但是也需要耗費時間。任務一旦多到一個限度,反而使得系統性能下降,導致所有任務都做不好。

第二個考慮是任務的類型,可以把任務分為計算密集型和 I/O 密集型。
計算密集型任務的特點是要進行大量的計算,消耗 CPU 資源,雖然也可以用多任務完成,但是任務越多,花在任務切換的時間就越多,CPU 執行任務的效率就越低。這類任務用 Python 執行效率通常很低,最能勝任這類任務的是 C 語言,而 Python 中有嵌入 C/C++ 程式碼的機制。

其他涉及到網絡、存儲介質 I/O 的任務都可以視為 I/O 密集型任務,這類任務的特點是 CPU 消耗很少,任務的大部分時間都在等待 I /O 操作完成, I/O 的速度遠遠低於 CPU 和內存的速度。對於 I/O 密集型任務,如果啟動多任務,就可以減少 I/O 等待時間從而讓 CPU 高效率的運轉。

單線程 (thread) + 異步 I/O

可以利用作業系統提供的異步 I/O 支持,就能用單行程 (process) 單線程 (thread) 來執行多任務,這種模型稱為事件驅動模型。Nginx 就是支持異步 I/O 的 Web 服務器,在單核 CPU 上採用單行程 (process) 模型就可以高效地支持多任務。在多核 CPU 上,可以運行多個行程 (process),充分利用多核 CPU。用 Node.js 開發的服務器端程序也使用了這種工作模式。

在 Python 語言中,單線程 (thread) + 異步 I/O 的編程模型稱為協程,可以基於事件驅動編寫高效的多任務程序。
協程最大的優勢就是極高的執行效率,因為子程序切換不是線程 (thread) 切換,而是由程序自身控制。第二個優勢是不需要多線程 (Multithreading) 的鎖機制,因為只有一個線程 (thread),不存在同時寫變量衝突,只需要判斷狀態就好,所以執行效率高很多。如果想充分利用 CPU 的多核特性,最簡單的方法是多行程 (Multi-Process) + 協程。

範例

範例1 - 將耗時間的任務放到線程 (thread) 中以獲得更好的用戶體驗。
有'下載'和'關於'兩個按鈕,用休眠的方式模擬點擊'下載'按鈕會連網下載文件需要耗費 10 秒,如果不使用多線程 (Multithreading),當點擊'下載'按鈕後整個程序的其他部分都被這個耗時間的任務阻塞而無法執行。

import time
import tkinter
import tkinter.messagebox

def download():
    # 模擬下載任務需要花費 10 秒
    time.sleep(10)
    tkinter.messagebox.showinfo('提示', '下載完成!')

def show_about():
    tkinter.messagebox.showinfo('關於', 'welcome')

def main():
    top = tkinter.Tk()
    top.title('單線程')          # 視窗標題
    top.geometry('250x150')      # 視窗大小
    top.wm_attributes('-topmost', True)  # 視窗彈出置頂的

    panel = tkinter.Frame(top)  
    button1 = tkinter.Button (panel, text = '下載', command = download)
    button1.pack(side = 'left')
    button2 = tkinter.Button (panel, text = '關於', command = show_about)
    button2.pack(side = 'right')
    panel.pack(side = 'bottom')

    tkinter.mainloop()

if __name__ == '__main__':
    main()

https://ithelp.ithome.com.tw/upload/images/20190927/20121116AIWcD5tX6u.png
如果使用多線程 (Multithreading) 將耗時間的任務放到一個獨立的線程 (thread) 中執行,這樣就不會因為執行耗時間的任務而阻塞了主線程 (thread) 。

import time
import tkinter
import tkinter.messagebox
from threading import Thread

def main():

    class DownloadTaskHandler(Thread):

        def run(self):
            time.sleep(10)
            tkinter.messagebox.showinfo('提示', '下載完成!')
            # 啟用下載按鈕
            button1.config(state = tkinter.NORMAL)

    def download():
        # 禁用下載按鈕
        button1.config(state = tkinter.DISABLED)
        # 透過 daemon 參數將線程 (thread) 設為守護線程 (thread), 
        # 主程式退出就不再保留執行
        # 在線程中處理耗時間的下載任務
        DownloadTaskHandler(daemon = True).start()

    def show_about():
        tkinter.messagebox.showinfo('關於', 'welcome')

    top = tkinter.Tk()
    top.title('單線程')          # 視窗標題
    top.geometry('250x150')      # 視窗大小
    top.wm_attributes('-topmost', 1)  # 視窗彈出置頂的

    panel = tkinter.Frame(top)
    button1 = tkinter.Button (panel, text = '下載', command = download)
    button1.pack(side = 'left')
    button2 = tkinter.Button(panel, text = '關於', command = show_about)
    button2.pack(side = 'right')
    panel.pack(side = 'bottom')

    tkinter.mainloop()


if __name__ == '__main__':
    main()

https://ithelp.ithome.com.tw/upload/images/20190927/20121116buScz4eAdk.png
圖中可以看出按鈕被進用了。
範例2 - 使用多行程 (Multi-Process)對複雜任務進行'分而治之'。
完成 1 ~ 100000000 求和的計算密集型任務。

from time import time

def main():
    total = 0
    number_list = [x for x in range(1, 100000001)]
    start = time()
    for number in number_list:
        total += number
    print(total)
    end = time()
    print('Execution time: %.3fs' % (end - start))

if __name__ == '__main__':
    main()

https://ithelp.ithome.com.tw/upload/images/20190927/20121116sE9JtNpBRd.png

from multiprocessing import Process, Queue
from random import randint
from time import time

def task_handler (curr_list, result_queue):
    total = 0
    for number in curr_list:
        total += number
    result_queue.put(total)

def main():
    processes = []
    number_list = [x for x in range(1, 100000001)]
    result_queue = Queue()
    index = 0
    # 啟動 8 個行程 (process) 將數據切片後進行運算
    for _ in range(8):
        p = Process (target = task_handler,
                    args = (number_list[index:index + 12500000], result_queue))
        index += 12500000
        processes.append(p)
        p.start()
    # 開始記錄所有行程 (process) 執行完成花費的時間
    start = time()
    for p in processes:
        p.join()
    # 執行结果
    total = 0
    while not result_queue.empty():
        total += result_queue.get()
    print(total)
    end = time()
    print('Execution time: ', (end - start), 's', sep = '')

if __name__ == '__main__':
    main()

https://ithelp.ithome.com.tw/upload/images/20190927/201211169tbMQlLMly.png
比較兩段程式碼的執行結果,使用多行程 (Multi-Process) 後由於獲得了更多的 CPU 執行時間以及更好的利用了 CPU 的多核特性,明顯的減少程式的執行時間,而且計算量越大效果越明顯。


上一篇
[Day13] 行程 (process) 和線程 (thread)
下一篇
[Day15] 網路編程入門和網路應用開發
系列文
30天學python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
laudai
iT邦新手 5 級 ‧ 2021-01-16 14:41:17

HI 版主你好

關於範例二,有些地方不解想跟你討論一下。
第一個是第24行的 start = time()
為什麼要放在那個位置,而非如同單行程的部分放在
number_list = [x for x in range(1, 100000001)] 的後方?
這樣兩者在算總體時間的時候不是基準點才相同嗎?
因為當你p.start() 你的process就會開始運行了,不會等到你join後才開始運行。

第二個問題是,如果以總體時間來看
此案例在multiprocess的情況下,雖然確實有使用多核,但時間反而花更多。
是因為在後續運算的時候,有使用到while 以及queue的呼叫嗎?
(下圖 test為單一process,test2為multiprocess)
linux time

我要留言

立即登入留言