+1

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ột Process, 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 1 shared 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

image.png

  • 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 core
  • Parallelism 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ùng multi-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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí