So sánh hiệu suất multi-thread và multi-process trong Python

  Apr 14, 2020      2m
   

Multithreading & Multiprocessing

So sánh hiệu suất multi-thread và multi-process trong Python

Multi-thread trong Python thực sự chưa giúp cải thiện hiệu suất chương trình nhiều, bởi tiến trình GIL (Global Interpreter Lock) đảm bảo chỉ cho một thread trong chương trình truy xuất biến dữ liệu. Điều này vô tình chặn các hoạt động của luồng xử lý (thread) khác trong chương trình (blocking I/O). Giải thích chi tiết bạn tham khảo thêm tại: https://www.quantstart.com/articles/Parallelising-Python-with-Threading-and-Multiprocessing/.

Ta có thể cải thiện hiệu suất chương trình Python để khai thác hết các core CPU xử lý bằng cách dùng multi-process (đa tiến trình). Lúc này, trong chương trình mình sẽ tạo thêm nhiều tiến trình con để độc lập xử lý các tác vụ. Trong Python có thư viện multiprocessing hỗ trợ việc này. Tốc độ xử lý multi-process thực sự nhanh hơn và hiệu suất cao hơn so với multi-thread.

Minh sẽ viết code thực nghiệm đo đạc đánh giá thời gian xử lý giữa dùng multi-thread và multi-process trong Python 3.

Hiện thực tác vụ xử lý

Mình sẽ hiện thực một tác vụ lý (tạm gọi là operation) tính toán tốn nhiều tài nguyên. Cụ thể là toán tử correlation / convolution (tham khảo bài viết: Xử lý ảnh - Convolution là gì?).

Một phép convolution này mặc định sẽ convolve ma trận 3x3 với một ảnh kích thước 128x128. Như vậy, số phép tính cần thiện hiện: 126 * 126 * (3 * 3 + 1) = 158760 phép toán (nhân và cộng). Vậy ta chỉ cần 10 operation này để có hơn 1 triệu phép tính.

operation.py

# -*- coding: utf-8 -*-
import random


def run_operation(
    num_op, matrix_size=128, kernel_size=3, float_from=-1.0, float_to=1.0
):
    for _ in range(num_op):
        op = Operation(matrix_size, kernel_size, float_from, float_to)
        op()
    pass


class Operation(object):
    def __init__(self, matrix_size=128, kernel_size=3, float_from=-1.0, float_to=1.0):
        self.matrix_size = matrix_size
        self.kernel_size = kernel_size
        self.float_from = float_from
        self.float_to = float_to

        assert self.matrix_size >= 3
        assert self.kernel_size >= 3
        assert self.matrix_size >= self.kernel_size
        pass

    def _init_matrix(self, size):
        matrix = []
        for r in range(size):
            one_row = []
            for c in range(size):
                one_row.append(random.uniform(self.float_from, self.float_to))
            matrix.append(one_row)
        return matrix

    def __call__(self):
        self.matrix = self._init_matrix(size=self.matrix_size)
        self.kernel = self._init_matrix(size=self.kernel_size)

        nloop = self.matrix_size - self.kernel_size + 1
        self.result = self._init_matrix(size=nloop)
        for my in range(nloop):
            for mx in range(nloop):
                for ky in range(self.kernel_size):
                    for kx in range(self.kernel_size):
                        kernel_val = self.kernel[ky][kx]
                        matrix_val = self.matrix[my + ky][mx + kx]
                        self.result[my][mx] = matrix_val * kernel_val
        return True

Hiện thực xử lý multi-thread trong Python

Tiếp đến, mình sẽ hiện thực multi-thread trong Python để chạy các operation trên. Tất cả các file Python các bạn để trong cùng thư mục nhé: operation.py, multi_thread.py, multi_process.py.

multi_thread.py

# -*- coding: utf-8 -*-
import time
import threading
from operation import run_operation

class MultiThread(object):
    def __init__(self, num_thread=4, num_op=100):
        self.num_thread = num_thread
        self.num_op = num_op
        assert self.num_thread > 0
        assert self.num_op > 0
        pass

    def __call__(self):
        thread_list = []
        for _ in range(self.num_thread):
            t = threading.Thread(target=run_operation, args=(self.num_op,))
            t.start()
            thread_list.append(t)

        for _ in range(len(thread_list)):
            t = thread_list[_]
            t.join()

        pass
    
def main(num_cpus=4, num_ops=10):
    tstart = time.time()
    multi = MultiThread(num_thread=num_cpus, num_op=num_ops)
    multi()
    tend = time.time()
    print("Time for running %d threads (%d ops) is %.2f seconds" % (num_cpus, num_ops, tend-tstart))
    
if __name__ == "__main__":
    main()    

Chạy script trên với câu lệnh và được kết quả in ra như bên dưới:

$ python3 multi_thread.py
Time for running 4 threads (10 ops) is 3.16 seconds

Vậy là mình mất 3.16 giây để xử lý 40 operation (4 thread, mỗi thread xử lý 10 operation)

Hiện thực xử lý multi-process trong Python

Tương tự, ta hiện thực tác vụ xử lý hệt như trên nhưng dùng multiprocessing.

multi_process.py

# -*- coding: utf-8 -*-
import time
from multiprocessing import Process
from operation import run_operation

class MultiProcess(object):
    def __init__(self, num_process=4, num_op=100):
        self.num_process = num_process
        self.num_op = num_op
        assert self.num_process > 0
        assert self.num_op > 0
        pass

    def __call__(self):
        process_list = []
        for _ in range(self.num_process):
            p = Process(target=run_operation, args=(self.num_op,))
            p.start()
            process_list.append(p)

        for _ in range(len(process_list)):
            p = process_list[_]
            p.join()

        pass
    
def main(num_cpus=4, num_ops=10):
    tstart = time.time()
    multi = MultiProcess(num_process=num_cpus, num_op=num_ops)
    multi()
    tend = time.time()
    print("Time for running %d processes (%d ops) is %.2f seconds" % (num_cpus, num_ops, tend-tstart))
    
if __name__ == "__main__":
    main()    

Chạy thử:

$ python3 multi_process.py
Time for running 4 processes (10 ops) is 1.34 seconds

Multi-process chỉ mất 1.34 giây để xử lý 40 operation (4 process, mỗi process xử lý 10 operation)

Chưa cần phân tích sâu thêm, ta cũng đã thấy rằng multi-process thực sự nhanh hơn multi-thread nhiều.

Thư viện đánh giá hiệu suất xử lý giữa multi-thread và multi-process

Minh cũng đã hiện thực việc đánh giá hiệu suất thành một gói thư viện Python để tiện việc chạy thử và đánh giá trên máy.

Cách sử dụng:

$ sudo pip3 install python_benchmark_thread_vs_process
$ python_benchmark_thread_vs_process
Benchmarking (4 CPUs @ 3559MHz) ... please wait...
====================
| BENCHMARK RESULT |
====================
+----------+-----------------------------------------+------------------------+-----------------------+------------------------+----------------------+
| Num CPUs | CPU Model                               | Current CPU Freq (MHz) | Multi-Thread Time (s) | Multi-Process Time (s) | Total Test Operation |
+----------+-----------------------------------------+------------------------+-----------------------+------------------------+----------------------+
| 4        | Intel(R) Core(TM) i5-2500 CPU @ 3.30GHz | 3559                   | 32.5341               | 13.1884                | 400                  |
+----------+-----------------------------------------+------------------------+-----------------------+------------------------+----------------------+

Lưu ý: chạy đánh giá bằng thư viện mất từ 1-5 phút để hoàn thành, vì số lượng operation test sẽ là 100 operation trên mỗi core CPU.

Nhận xét việc dùng multi-thread và multi-process

Minh đã chạy thư viện đánh giá trên nhiều máy tính, server mà mình được phép truy cập để có bảng đánh giá so sánh hiệu suất giữa multi-thread và multi-process như dưới đây:

Num CPUsCPU ModelCurrent CPU Freq (MHz)Multi-Thread Time (s)Multi-Process Time (s)Total Test Operation
1Intel(R) Xeon(R) CPU E5-2680 v3 @ 2.50GHz250011.758112.0673100
4Intel(R) Core(TM) i5-2500 CPU @ 3.30GHz247455.38408.8589400
4Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz268320.909810.9195400
16Intel(R) Xeon(R) CPU E5-2640 v3 @ 2.60GHz259798.65847.10331600
24Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz1331372.392618.59232400
32Intel(R) Xeon(R) Silver 4108 CPU @ 1.80GHz809478.811515.05383200
72Intel(R) Xeon(R) Gold 5220S CPU @ 2.70GHz1016550.493611.67597200

Nhận xét multi-thread và multi-proces trong Python:

  • Cấu hình máy 1 core CPU, xử lý đơn luồng (single thread, chỉ có main thread), đa luồng (multi-thread) hay đa tiến trình (multi-process) đều sẽ cho kết quả như nhau. Thậm chí, multi-process sẽ chậm hơn một chút cho chi phí khởi tạo tiến trình lớn hơn khởi tạo luồng. Trong Python, thread còn được gọi là light-weight process (https://docs.python.org/3/library/_thread.html).
  • Tốc độ xử lý phụ thuộc vào cấu hình phần cứng (CPU model, CPU Frequency MHz, số core CPU) cũng như mức độ bận rộn của tài nguyên máy tính ở thời điểm test.
  • Đa tiến trình (multi process) nhanh hơn đa luồng (multi thread) trong Python, do đa tiến trình khai thác được tính toán song song đa lõi của CPU.
  • Multi thread chia sẻ bộ nhớ giữa các thread con dễ dàng hơn so với process, do đó developer dễ hiện thực hơn, để có thể chia sẻ giữa các process cần hiện thực "công phu" hơn. Tham khảo thêm thư viện multiprocessing của Python: https://docs.python.org/3/library/multiprocessing.html

Bài viết về Python:

Tham gia ngay group trên Facebook để cùng thảo luận với đồng bọn nhé: