Concurrency và Parallelism trong Python
Hồi mới đầu làm data crawling, mình hay thắc mắc tại sao trên mạng hướng dẫn toàn dùng Multithread chứ không dùng Multiprocess. Thế là xong cũng mới đi tìm hiểu 2 khái niệm này, và đọc 2 cơ chế Parallelism
hay Concurrency
. Vậy thì trong bài này, cùng tìm hiểu 2 concept này khác nhau thế nào với 1 ngôn ngữ cụ thể là Python nhé.
Overview Process và Thread
Như đã học môn Nguyên lý hệ điều hành,
Process
là một tiến trình, là bất kì 1 program nào đang chạy. Ví dụ, khi mở một ứng dụng, thì process chạy ứng dụng đó sẽ được khởi tạo và chạy. Hệ điều hành sẽ khởi tạo, lên lịch, chạy và tắt các process. Các process chạy riêng biệt với nhau.Thread
là một luồng bên trong mộtProcess
, và mỗi process có thể có nhiều thread, và các thread có thể truy cập và sử dụng cùng 1shared resource
Python GIL
Về cơ bản, GIL là cơ chế quản lý luồng của python, và nó đưa ra 1 luật là mỗi process chỉ được phép có duy nhất 1 thread chạy tại 1 thời điểm xác định. Mục đích chính của GIL là nhằm tránh Race condition
. Race condition
có thể được hiểu là nhiều tiến trình cùng sửa đổi 1 shared resource, dẫn đến sự bất đồng bộ.
Vì vậy, để chạy 1 thread mới thì phải giải phóng thread đang chạy, dẫn đến việc nếu sử dụng multi-threading ko hợp lí thì sẽ làm giảm performance thay vì tăng.
Concurrency và Parallelism
Concurrency
là >= 2 tiến trình được chạy xen kẽ (interleave) với cơ chế context switching của OS trên 1 coreParallelism
là >= 2 tiến trình chạy song song trên nhiều core
CPU-bound và IO-bound
- CPU-bound là tiến trình sử dụng CPU phần lớn (tính toán, xử lý dữ liệu, ...). Vậy nên để tăng performance cho những tiến trình dạng này, thì phải tăng số lượng phép tính hơn trong cùng 1 khoảng thời gian
=> Cần dùng cơ chế Parallelism - IO-bound là tiến trình mà phần lớn thời gian là dành cho các task IO, như đọc ghi file, send request/wait response, ... khi này CPU sẽ không cần thiết
=> Tiến trình có thể được lưu và tắt đi tạm thời, khi task IO xong có thể được restore
=> Có thể dùng cơ chế Concurrency để chạy 1 tiến trình khác một cách xen kẽ, trong thời gian đợi IO của tiến trình kia
Thực nghiệm
Từ những thứ đã viết ở trên, áp dụng với Python:
- Với các function chức năng chính là tính toán (cần dùng CPU), thì dùng
multi-processing
để tăng performance - Với các function chức năng chính là đọc ghi (read/write file, send request, wait response, ...), thì có thể dùng
multi-threading
để tăng performance, tất nhiên là dùngmulti-processing
thì cũng ko sao 😁
Giờ test thử cái xem có thật là thế ko nhé. Với 2 cái hàm io
là sleep sec
giây và cpu
là đếm ngược từ n
-> 0 (cần tính toán)
import os
import time
from threading import Thread, current_thread
from multiprocessing import Process, current_process
C = 100000000
SLEEP_TIME = 5
def io(sec: float):
pid, thread_name, process_name = os.getpid(), current_thread().name, current_process().name
print(f"# PID: {pid} | PROCESS: {process_name} | THREAD: {thread_name} ### START IO...")
time.sleep(sec)
print(f"# PID: {pid} | PROCESS: {process_name} | THREAD: {thread_name} ### DONE IO...")
def cpu(n: float):
pid, thread_name, process_name = os.getpid(), current_thread().name, current_process().name
print(f"# PID: {pid} | PROCESS: {process_name} | THREAD: {thread_name} ### START CPU...")
while (n >= 0):
n -= 1
print(f"# PID: {pid} | PROCESS: {process_name} | THREAD: {thread_name} ### DONE CPU...")
Chạy tuần tự hàm io
start = time.time()
io(SLEEP_TIME)
io(SLEEP_TIME)
print(f'Finish all in {round(time.time() - start, 2)}s')
### kết quả ###
# PID: 44874 - PROCESS: MainProcess - THREAD: MainThread ### START IO...
# PID: 44874 - PROCESS: MainProcess - THREAD: MainThread ### DONE IO...
# PID: 44874 - PROCESS: MainProcess - THREAD: MainThread ### START IO...
# PID: 44874 - PROCESS: MainProcess - THREAD: MainThread ### DONE IO...
# Finish all in 10.01s
Chạy hàm io
với multi-threading và multi-processing
start = time.time()
t1 = Thread(target = io, args =(SLEEP_TIME, ))
t2 = Thread(target = io, args =(SLEEP_TIME, ))
t1.start()
t2.start()
t1.join()
t2.join()
print(f'Multi-threading io function finish all in {round(time.time() - start, 2)}s')
start = time.time()
p1 = Process(target = io, args =(SLEEP_TIME, ))
p2 = Process(target = io, args =(SLEEP_TIME, ))
p1.start()
p2.start()
p1.join()
p2.join()
print(f'Multi-processing io function finish all in {round(time.time() - start, 2)}s')
### kết quả ###
# PID: 52808 | PROCESS: MainProcess | THREAD: Thread-1 (io) ### START IO...
# PID: 52808 | PROCESS: MainProcess | THREAD: Thread-2 (io) ### START IO...
# PID: 52808 | PROCESS: MainProcess | THREAD: Thread-1 (io) ### DONE IO...
# PID: 52808 | PROCESS: MainProcess | THREAD: Thread-2 (io) ### DONE IO...
# Multi-threading io function finish all in 5.01s
# PID: 53160 | PROCESS: Process-1 | THREAD: MainThread ### START IO...
# PID: 53161 | PROCESS: Process-2 | THREAD: MainThread ### START IO...
# PID: 53161 | PROCESS: Process-2 | THREAD: MainThread ### DONE IO...
# PID: 53160 | PROCESS: Process-1 | THREAD: MainThread ### DONE IO...
# Multi-processing io function finish all in 5.02s
Có thể thấy, với hàm io
là IO-bound, nên multi-threading và multi-processing đều đạt được kết quả tương tự nhau
Giờ test thử hàm io
với chạy tuần tự, multi-threading và multi-processing
start = time.time()
cpu(C)
cpu(C)
print(f'# Sequential execution finish all in {round(time.time() - start, 2)}s')
start = time.time()
t1 = Thread(target = cpu, args =(C, ))
t2 = Thread(target = cpu, args =(C, ))
t1.start()
t2.start()
t1.join()
t2.join()
print(f'Multi-threading io function finish all in {round(time.time() - start, 2)}s')
start = time.time()
p1 = Process(target = cpu, args =(C, ))
p2 = Process(target = cpu, args =(C, ))
p1.start()
p2.start()
p1.join()
p2.join()
print(f'Multi-processing io function finish all in {round(time.time() - start, 2)}s')
### kết quả ###
# PID: 65034 | PROCESS: MainProcess | THREAD: MainThread ### START CPU...
# PID: 65034 | PROCESS: MainProcess | THREAD: MainThread ### DONE CPU...
# PID: 65034 | PROCESS: MainProcess | THREAD: MainThread ### START CPU...
# PID: 65034 | PROCESS: MainProcess | THREAD: MainThread ### DONE CPU...
# Sequential execution finish all in 4.65s
# PID: 65034 | PROCESS: MainProcess | THREAD: Thread-1 (cpu) ### START CPU...
# PID: 65034 | PROCESS: MainProcess | THREAD: Thread-2 (cpu) ### START CPU...
# PID: 65034 | PROCESS: MainProcess | THREAD: Thread-1 (cpu) ### DONE CPU...
# PID: 65034 | PROCESS: MainProcess | THREAD: Thread-2 (cpu) ### DONE CPU...
# Multi-threading io function finish all in 4.32s
# PID: 65271 | PROCESS: Process-1 | THREAD: MainThread ### START CPU...
# PID: 65272 | PROCESS: Process-2 | THREAD: MainThread ### START CPU...
# PID: 65271 | PROCESS: Process-1 | THREAD: MainThread ### DONE CPU...
# PID: 65272 | PROCESS: Process-2 | THREAD: MainThread ### DONE CPU...
# Multi-processing io function finish all in 2.57s
Có thể thấy rằng vì hàm này là CPU-bound, nên chạy multi-threading sẽ chạy theo kiểu xen kẽ, dẫn đến performance tương tự với chạy tuần tự. Ngược lại, multi-processing tăng performance gần gấp đôi, do việc chạy 2 tiến trình song song với nhau thay vì tuần tự hay xen kẽ.
Chốt lại, đối với các task CPU-bound thì nên dùng multi-processing, ví dụ: tính toán, xử lý dữ liệu, v.v. Đối với các task IO-bound thì có thể dùng multi-threading, ví dụ: read/write file, crawl web, gọi API, ...
All rights reserved