Try   HackMD

[python - speed up]


Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Threads and Processes

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

javatpoint.com/process-vs-thread

在現代的電腦科學和軟體開發中,善用多核心電腦的 processthread 可以達到以下功能:

  • 效能提升:透過合理的使用多進程和多執行緒,我們可以讓軟體運行得更快,更有效地利用系統資源。

  • 用戶體驗:多進程和多執行緒可以讓軟體在執行耗時的任務時,仍然保持對用戶的響應,例如,可以在一個執行緒下載文件,同時在另一個執行緒繼續瀏覽網頁。

  • 解決複雜問題:對於一些複雜的問題,例如大數據分析或複雜的科學計算,多進程和多執行緒提供了一種分而治之的方法,使得問題更易於處理。

一個process就像是一家餐廳。這家餐廳(process)有它自己的資源:廚房(CPU)、食材(記憶體)、廚具(I/O 設備)等等。每一家餐廳都獨立運作,不會影響到其他餐廳。而thread執行緒,則可以想像成是餐廳裡的廚師。一個餐廳(process)可以有一個或多個廚師(執行緒):

單執行緒(single thread):就像一家只有一個廚師的餐廳。無論多少訂單(任務),都只能由這一個廚師依序製作。如果這個廚師在煮一道需要長時間等待的菜(例如:慢燉牛肉),那麼其他的訂單就必須等待。

多執行緒(multi thread):就像一家有多個廚師的餐廳。他們可以同時製作多個訂單,提高效率。但是,如果不協調好,比如說兩個廚師同時使用同一個鍋子(共享資源),就可能會出現問題(競態條件Race Condition)

1. 定義、適用任務類型、具體情境、優缺點

差異點 Process (程序/進程) Thread (執行緒/線程)
定義 進程是一個正在執行的程式 執行緒是進程的一小部分,它可以獨立運行
是否共享記憶體 彼此不共享記憶體空間 執行緒會共享
同一進程內的記憶體空間
作業系統的處理方式 作業系統把每個進程
當作一個單獨的個體
作業系統把同一進程內
的所有執行緒視為一個單位
阻塞
Blocking
一個進程阻塞不會影響其他進程 一個執行緒阻塞可能導致同一
進程內的所有執行緒都無法進行
切換速度
Context Switching
相對執行序"重",切換慢 執行緒之間切換快,因為非常輕量
資料和程式碼段 進程間的資料和程式碼是獨立的 執行緒會共享資料和程式碼區塊
終止時間 通常較為耗時 可以迅速終止
創建時間 較為耗時,因為每個新進程都需要所有資源。 迅速

修改自javatpoint.com/process-vs-thread

  • 按照使用情境整理
項目 Process (程序/進程) Thread (執行緒/線程)
定義 進程是一個獨立執行的程式實例,擁有自己的記憶體空間。 執行緒是進程內的一個單一執行流程,共享該進程的記憶體空間。
適用任務類型 適合CPU密集型(CPU-Bound)任務,或需要獨立記憶體空間的任務。 適合I/O密集(I/O-Bound)任務,或需要多任務並行處理的情境。
具體情境 - 運行不同的應用程式
- 需要隔離記憶體空間的任務
- GUI 應用程式,需要同時處理用戶輸入和界面更新
- 伺服器應用程式,需要同時處理多個客戶端的請求
優點 - 較高的記憶體隔離,安全性較高
- 進程間不會互相影響
- 執行緒間資料共享較為方便
- 創建和切換執行緒的成本較低
缺點 - 創建和切換進程的成本較高
- 進程間通信 (IPC) 較為複雜
- 資料共享可能導致同步問題
- 過多的執行緒可能導致調度 overhead

python multi-thread與 multi-process模擬實測

n of
process
/thread
results
1
Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →
4
Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →
8
Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

ps:測試筆電i5-10210U是4核8執行緒,因此分別用n=4、8來測試

  • 在thread與process 數量跟硬體邏輯核心(執行緒)數量一致時,加速效果最明顯
  • 在很單純的模擬情境下,可以見到
    • multi-thread在I/O密集(I/O-Bound)任務加速效果好
    • multi-process則在CPU密集(CPU-Bound)任務加速效果好

這邊選用的範例是很不需任何訊息交換、可以單純呈現thread與process特性的模擬任務,實際表現要根據任務的效能瓶頸與具體執行情境而定

  • 模擬I/O密集與CPU密集的任務

    • 採用time.sleep(0.1)模擬I/O密集任務
    • 採用平方運算模擬CPU密集任務
    ​​​​import time
    ​​​​import threading
    ​​​​import multiprocessing
    ​​​​from multiprocessing import Pool
    ​​​​import requests   
    ​​​​import matplotlib.pyplot as plt
    ​​​​import seaborn as sns
    ​​​​import pandas as pd
    
    ​​​​# I/O-bound task: Simulating a network request with time.sleep()
    ​​​​def simulated_io_task():
    ​​​​    # Simulating I/O operation by sleeping for 0.1 seconds
    ​​​​    time.sleep(0.1)
    
    ​​​​# CPU-bound task: Calculating the power of large numbers
    ​​​​def simulated_cpu_task(number=99999):
    ​​​​    # Calculating power of a large number, which is CPU-intensive
    ​​​​    return number ** number
    
  • 測量任務計時

    • multi-thread與multi-process的計時,分別使用最簡單的threading.Threadmultiprocessing.Process,開n個thread與process
    • direct則直接把函式重複執行n次後計時
    code of Timing
    ​​​​# Timing functions
    ​​​​def time_task(task, method, num_tasks):
    ​​​​​   # Recording the start time
    ​​​​​   start_time = time.time()
    
    ​​​​​   if method == "threading":
    ​​​​​       # Creating threads and starting them
    ​​​​​       threads = [threading.Thread(target=task) for _ in range(num_tasks)]
    ​​​​​       [t.start() for t in threads]
    ​​​​​       [t.join() for t in threads]  # Waiting for all threads to finish
    ​​​​​   elif method == "multiprocessing":
    ​​​​​       # Creating processes and starting them
    ​​​​​       processes = [multiprocessing.Process(target=task) for _ in range(num_tasks)]
    ​​​​​       [p.start() for p in processes]
    ​​​​​       [p.join() for p in processes]  # Waiting for all processes to finish
    ​​​​​       # More Automated Uses
    ​​​​​       # Note: Ensure that your function `task` accepts an argument, even if it's not used, 
    ​​​​​       # because `pool.map` will pass elements from the iterable (here, `range(num_tasks)`) to it.
    ​​​​​       # 
    ​​​​​       # with Pool(processes=num_tasks) as pool:
    ​​​​​       #     results = pool.map(task, range(num_tasks))
    ​​​​​   else:  # Direct method
    ​​​​​       # Directly executing the task without threading or multiprocessing
    ​​​​​       [task() for _ in range(num_tasks)]
    
    ​​​​​   # Recording the end time
    ​​​​​   end_time = time.time()
    
    ​​​​​   # Returning the elapsed time in milliseconds
    ​​​​​   return (end_time - start_time) * 1000
    
    ​​​​# Timing data
    ​​​​core = 4
    ​​​​data = {
    ​​​​​   'Task Type': ['I/O-bound'] * 3 + ['CPU-bound'] * 3,
    ​​​​​   'Method': ['Direct', 'Multi-threading', 'Multi-processing'] * 2,
    ​​​​​   'Time (ms)': [
    ​​​​​       time_task(simulated_io_task, "direct", 8),
    ​​​​​       time_task(simulated_io_task, "threading", 8),
    ​​​​​       time_task(simulated_io_task, "multiprocessing", 8),
    ​​​​​       time_task(simulated_cpu_task, "direct", 8),
    ​​​​​       time_task(simulated_cpu_task, "threading", 8),
    ​​​​​       time_task(simulated_cpu_task, "multiprocessing", 8)
    ​​​​​   ]
    ​​​​}
    
  • 繪圖

    code of plot
    
    ​​​​# Create a DataFrame
    ​​​​df = pd.DataFrame(data)
    
    ​​​​# Create Seaborn plots
    ​​​​sns.set_theme(style="whitegrid")
    ​​​​fig, axes = plt.subplots(2, 1, figsize=(6, 4), sharex=True)  # Sharing the x-axis
    
    ​​​​# Define a color palette with soft, pastel colors
    ​​​​palette = sns.color_palette("pastel")
    
    ​​​​# Subplot for I/O-bound tasks
    ​​​​sns.barplot(ax=axes[0], x='Method', y='Time (ms)', data=df[df['Task Type'] == 'I/O-bound'], palette=palette)
    ​​​​axes[0].set_title(' I/O-bound tasks')
    ​​​​axes[0].set_ylabel('Time (ms)')
    
    ​​​​# Subplot for CPU-bound tasks
    ​​​​sns.barplot(ax=axes[1], x='Method', y='Time (ms)', data=df[df['Task Type'] == 'CPU-bound'], palette=palette)
    ​​​​axes[1].set_title(' CPU-bound tasks')
    ​​​​axes[1].set_ylabel('Execution Time (ms)')
    
    ​​​​# Adjusting the layout to prevent overlap
    ​​​​plt.tight_layout()
    ​​​​plt.show()
    

補充

根據python Concurrent Execution官方文件提到:

  • multiprocessing — Process-based parallelism

    • concurrent.futures.ProcessPoolExecutor 提供了一個更高層級的介面用來將任務推送到後台進程而不會阻塞調用方進程的執行。 與直接使用Pool介面相比,concurrent.futures API 能更好地允許將工作單元發往無需等待結果的下層進程池
  • threading — Thread-based parallelism

    • concurrent.futures.ThreadPoolExecutor 提供了一個高層級介面用來向後台線程推送任務而不會阻塞調用方線程的執行,同時仍然能夠在需要時獲取任務的結果。
    • queue 提供了一個線程安全的介面用來在運行中的線程之間交換數據。
    • asyncio 提供了一個替代方式用來實現任務層級的併發而不要求使用多個操作系統線程。

詳細操作見concurrent.futures — Launching parallel tasks一節說明

Reference