Chuyển đến nội dung chính

Đa xử lý trong Python: Hướng dẫn về luồng và tiến trình

Tìm hiểu cách quản lý luồng và tiến trình với mô-đun multiprocessing của Python. Khám phá các kỹ thuật then chốt cho lập trình song song. Nâng cao hiệu quả mã của bạn với các ví dụ.
· 7 phút đọc

Khám phá cùng AI

Thư viện chuẩn của Python đi kèm với nhiều gói dựng sẵn để nhà phát triển có thể tận dụng lợi thế của ngôn ngữ ngay lập tức.

Một trong số đó là mô-đun multiprocessing, cho phép hệ thống chạy đồng thời nhiều tiến trình. Nói cách khác, nhà phát triển có thể chia nhỏ ứng dụng thành các luồng có thể chạy độc lập với mã Python của họ. Các luồng hoặc tiến trình này sau đó được hệ điều hành phân bổ cho bộ xử lý, cho phép chúng chạy song song, cải thiện hiệu năng và hiệu quả cho chương trình Python của bạn. 

Nếu các thuật ngữ như luồng, tiến trình, bộ xử lý, v.v. còn xa lạ với bạn thì đừng lo. Trong bài viết này, chúng ta sẽ đề cập đến định nghĩa của tiến trình, cách chúng khác với luồng, và cách sử dụng mô-đun multiprocessing. 

Tiến trình trong Python là gì?

Hiểu khái niệm về tiến trình và luồng cực kỳ hữu ích để nắm rõ cách hệ điều hành quản lý chương trình qua các giai đoạn thực thi khác nhau. Một tiến trình chỉ đơn thuần là tham chiếu đến một chương trình máy tính. Mỗi chương trình có một tiến trình gắn với nó. 

Khi bạn đang đọc bài này, rất có thể máy tính của bạn có nhiều tiến trình đang chạy tại bất kỳ thời điểm nào, ngay cả khi bạn chỉ mở vài chương trình – do hầu hết hệ điều hành có nhiều tác vụ chạy nền. Ví dụ, bạn có thể chỉ có ba chương trình đang chạy lúc này, nhưng máy bạn có thể có hơn 30 tiến trình hoạt động đồng thời. 

Cách kiểm tra các tiến trình đang hoạt động trên máy tính phụ thuộc vào hệ điều hành của bạn: 

  • Trên Windows: Ctrl+Shift+Esc sẽ mở Trình quản lý tác vụ 
  • Trên Mac: mở Spotlight Search và gõ "Activity Monitor", sau đó nhấn Return  
  • Trên Linux: nhấp vào Application Menu và tìm System Monitor. 

Python cung cấp quyền truy cập đến các tiến trình ở cấp hệ thống thực. Khởi tạo một thể hiện của lớp Process từ mô-đun multiprocessing cho phép nhà phát triển tham chiếu đến tiến trình gốc bên dưới bằng Python. Một tiến trình gốc mới được tạo ở hậu trường khi một tiến trình được khởi động. Vòng đời của một tiến trình Python gồm ba giai đoạn: khởi tạo tiến trình mới, tiến trình đang chạy và tiến trình đã kết thúc – chúng ta sẽ đề cập từng giai đoạn.

Mọi tiến trình đều gồm một hoặc nhiều luồng. Bạn còn nhớ chúng ta đã nói rằng “một tiến trình chỉ đơn thuần là tham chiếu đến một chương trình máy tính” chứ? Vâng, mỗi chương trình Python là một tiến trình gồm một luồng mặc định gọi là luồng chính (main thread). Luồng chính chịu trách nhiệm thực thi các chỉ dẫn trong chương trình Python của bạn. Tuy nhiên, cần lưu ý rằng tiến trình và luồng là khác nhau. 

Multiprocessing so với Threading

Cụ thể hơn, một thể hiện của trình thông dịch Python – công cụ chuyển đổi mã viết bằng Python sang ngôn ngữ máy tính có thể hiểu – tương ứng với một tiến trình. Một tiến trình sẽ gồm ít nhất một luồng, gọi là “luồng chính” trong Python, dù có thể tạo thêm các luồng khác trong cùng tiến trình – mọi luồng khác được tạo trong một tiến trình sẽ thuộc về tiến trình đó. 

Luồng là đại diện cho cách chương trình Python của bạn sẽ được thực thi, và khi tất cả các luồng không chạy nền kết thúc, tiến trình Python sẽ chấm dứt. 

  • Process (Tiến trình): Một tiến trình là một thể hiện của trình thông dịch Python, gồm ít nhất một luồng gọi là luồng chính. 
  • Thread (Luồng): Đại diện cho cách một chương trình Python được thực thi trong một tiến trình Python.

Python có hai lớp rất giống nhau cho phép chúng ta kiểm soát nhiều hơn đối với tiến trình và luồng: multiprocessing.Process và threading.Thread.

Hãy cùng xem một số điểm tương đồng và khác biệt của chúng. 

Tương đồng

Tính đồng thời (Concurrency)

Tính đồng thời là khái niệm trong đó các phần khác nhau của chương trình có thể được thực thi không theo thứ tự hoặc theo thứ tự một phần mà không ảnh hưởng đến kết quả cuối cùng. Cả hai lớp ban đầu được thiết kế cho tính đồng thời. 

Hỗ trợ các nguyên thủy đồng bộ

Các lớp multiprocessing.Process và threading.Thread hỗ trợ cùng các nguyên thủy đồng bộ – công cụ cho phép đồng bộ và phối hợp giữa luồng và tiến trình. 

API thống nhất

Một khi bạn nắm vững API của multiprocessing.Process, bạn có thể chuyển giao kiến thức đó sang API của threading.Thread và ngược lại. Chúng được cố ý thiết kế như vậy. 

Khác biệt

Chức năng

Mặc dù API giống nhau, tiến trình và luồng vẫn khác nhau. Tiến trình là mức trừu tượng cao hơn luồng: tiến trình là tham chiếu đến chương trình máy tính, còn luồng thuộc về một tiến trình. Sự khác biệt này vốn có trong các lớp. Do đó, các lớp đại diện cho hai chức năng gốc khác nhau do hệ điều hành cơ sở quản lý. 

Truy cập trạng thái dùng chung

Hai lớp truy cập trạng thái dùng chung theo cách khác nhau. Vì luồng thuộc về một tiến trình, chúng có thể chia sẻ bộ nhớ trong tiến trình. Do đó, một hàm được thực thi trong luồng mới vẫn có quyền truy cập dữ liệu và trạng thái giống nhau trong tiến trình. Cách các luồng chia sẻ trạng thái với nhau được gọi là “bộ nhớ dùng chung” và khá trực quan. Ngược lại, chia sẻ trạng thái giữa các tiến trình phức tạp hơn nhiều: trạng thái phải được tuần tự hóa và truyền giữa các tiến trình. Nói cách khác, tiến trình không dùng bộ nhớ chung để chia sẻ trạng thái vì chúng có bộ nhớ tách biệt. Thay vào đó, tiến trình chia sẻ với nhau bằng kỹ thuật gọi là “giao tiếp giữa các tiến trình” (inter-process communication), và để thực hiện trong Python cần các công cụ tường minh khác như multiprocessing.Pipe hoặc multiprocessing.Queue. 

GIL

Global Interpreter Lock (GIL) của Python là một khóa chỉ cho phép một luồng nắm quyền điều khiển trình thông dịch Python tại một thời điểm. Nhiều luồng chịu sự chi phối của GIL, điều này thường khiến việc dùng Python để đa luồng không phải ý hay: thực thi đa lõi thực sự thông qua đa luồng không được hỗ trợ trong trình thông dịch CPython. Tuy nhiên, tiến trình không bị GIL chi phối vì GIL được dùng trong mỗi tiến trình Python chứ không dùng xuyên qua các tiến trình. 

Về trường hợp sử dụng, đa xử lý thường vượt trội so với đa luồng trong các kịch bản chương trình nặng CPU và không cần thực hiện IO hay tương tác người dùng. Đa luồng phù hợp hơn với các chương trình bị giới hạn bởi IO hoặc mạng và trong các tình huống mục tiêu là làm cho ứng dụng phản hồi tốt hơn. 

Lợi ích của đa xử lý trong Python

Hãy hình dung bộ xử lý như một doanh nhân. Khi doanh nghiệp của doanh nhân phát triển, có nhiều tác vụ hơn cần được quản lý để theo kịp đà tăng trưởng. Nếu doanh nhân quyết định tự mình đảm nhận tất cả các tác vụ này (ví dụ: kế toán, bán hàng, marketing, đổi mới, v.v.), cô ấy có nguy cơ làm giảm hiệu quả và hiệu suất tổng thể của doanh nghiệp vì một người chỉ có thể làm được từng ấy việc tại một thời điểm. Ví dụ, trước khi chuyển sang các nhiệm vụ đổi mới, cô ấy phải dừng các nhiệm vụ bán hàng - điều này được gọi là chạy tác vụ theo “tuần tự.” 

Hầu hết doanh nhân hiểu rằng cố làm mọi thứ một mình là một ý tưởng tệ. Do đó, họ thường san sẻ số lượng tác vụ ngày càng tăng bằng cách thuê nhân viên quản lý các bộ phận khác nhau. Bằng cách này, các tác vụ có thể được thực hiện song song - nghĩa là không cần dừng một tác vụ để chạy tác vụ khác. Việc thuê thêm nhân viên để thực hiện các tác vụ cụ thể tương tự như sử dụng nhiều bộ xử lý để thực hiện thao tác. Ví dụ, các dự án thị giác máy tính khá nặng vì bạn thường phải xử lý rất nhiều dữ liệu ảnh, tốn thời gian: để tăng tốc, bạn có thể xử lý nhiều ảnh song song. 

Vì vậy, có thể nói đa xử lý hữu ích để làm cho chương trình hiệu quả hơn bằng cách chia nhỏ và phân công tác vụ cho các bộ xử lý khác nhau. Mô-đun multiprocessing của Python còn đơn giản hóa hơn nữa khi đóng vai trò là công cụ cấp cao để tăng hiệu quả chương trình của bạn bằng cách phân công tác vụ cho các tiến trình khác nhau. 

Những điều cơ bản về mô-đun Multiprocessing của Python 

Chúng ta đã đề cập vòng đời của một tiến trình Python gồm ba giai đoạn: tiến trình mới, tiến trình đang chạy và tiến trình đã kết thúc. Phần này sẽ đi sâu vào từng giai đoạn của vòng đời và cung cấp ví dụ mã. Bạn có thể truy cập mã để thực hành theo bằng cách vào sổ tay DataLab của chúng tôi. 

Tiến trình mới 

Tiến trình mới có thể được hiểu là tiến trình được tạo ra bằng cách khởi tạo một thể hiện của lớp Process. Một tiến trình con được tạo ra khi chúng ta gán đối tượng Process cho một biến. 

from multiprocessing import Process

# Create a new process
process = Process()

Hiện tại, thể hiện tiến trình của chúng ta chưa làm gì vì chúng ta khởi tạo một đối tượng Process trống. Chúng ta có thể thay đổi cấu hình của đối tượng Process để thực thi một hàm cụ thể bằng cách truyền hàm muốn chạy trong một tiến trình khác vào tham số target của lớp. 

# Create a new process with a specified function to execute.
def example_function():
    pass

new_process = Process(target=example_function)

Nếu hàm đích của chúng ta cũng có tham số thì chỉ cần truyền chúng vào tham số args của đối tượng Process dưới dạng bộ (tuple).

Mẹo: Học cách viết hàm trong Python với khóa học tương tác Writing Functions in Python. 

# Create a new process with specific function to execute with args.
def example(args):
    pass

process = Process(target=example, args=("Hi",))

Lưu ý rằng chúng ta mới chỉ tạo tiến trình, nhưng nó chưa chạy. 

Hãy xem cách chúng ta có thể chạy một tiến trình mới.  

Tiến trình đang chạy 

Chạy một tiến trình mới khá đơn giản: chỉ cần gọi phương thức start() của thể hiện tiến trình. 

# Run the new process
process.start()

 Hành động này bắt đầu hoạt động của tiến trình bằng cách gọi phương thức run() của thể hiện Process ở phía dưới. Phương thức run() cũng chịu trách nhiệm gọi hàm tùy chỉnh được chỉ định trong tham số target của thể hiện Process (nếu có).

Hãy nhớ rằng ở đầu hướng dẫn, chúng ta đã nêu mỗi tiến trình có ít nhất một luồng gọi là luồng chính - đây là mặc định. Vì vậy, khi một tiến trình con được khởi động, luồng chính được tạo cho tiến trình con đó và được khởi động. Luồng chính chịu trách nhiệm thực thi toàn bộ mã của chúng ta trong tiến trình con. 

Chúng ta có thể kiểm tra thể hiện tiến trình đang sống từ khi phương thức start() trả về cho đến khi tiến trình con kết thúc bằng cách dùng phương thức is_alive() trên thể hiện Process. 

Lưu ý: Nếu bạn thử trong notebook, lời gọi is_alive() phải nằm trong cùng ô với lời gọi phương thức start() thì mới bắt được tiến trình đang chạy. 

# Run the new process
process.start()

# Check process is running
process.is_alive()

"""
True
"""

Nếu tiến trình không chạy, lời gọi phương thức is_alive() sẽ trả về False. 

Tiến trình đã kết thúc

Khi hàm run() trả về hoặc thoát, tiến trình sẽ kết thúc: chúng ta không nhất thiết phải làm gì thêm. Nói cách khác, bạn có thể kỳ vọng một tiến trình sẽ kết thúc sau khi các chỉ dẫn bạn đặt làm hàm đích hoàn tất. Do đó, phương thức is_alive() sẽ trả về false. 

# Check process is terminated - should return false.
process.is_alive()

"""
False
"""

Tuy vậy, một tiến trình cũng có thể kết thúc nếu gặp ngoại lệ không được xử lý hoặc phát sinh lỗi. Ví dụ, nếu có lỗi trong hàm bạn đặt làm đích, tiến trình sẽ kết thúc. 

# Create a new process with a specific function that has an error
# to execute with args.
def example(args):
    split_args = list(args.split())
    # "name" variable is not in the function namespace - should raise error
    return name

# New process
process = Process(target=example, args=("Hi",))

# Running the new process
process.start()

# Check process is running
process.is_alive()

"""
True

Process Process-15:
Traceback (most recent call last):
  File "/usr/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
"""

Đối tượng Process cũng có các phương thức terminate() và kill() cho phép người dùng buộc kết thúc một tiến trình. 

# Create a new process with a specific function to execute with args.
def example(args):
    split_args = list(args.split())
    # "name" variable is not in the function namespace - should raise error
    return split_args

# New process
process = Process(target=example, args=("Hi",))

# Running the new process
process.start()

if process.is_alive():
    process.terminate() # You can also use process.kill()
    print("Process terminated forcefully")

"""
Process terminated forcefully
"""

Cần lưu ý rằng việc buộc kết thúc không phải là cách khuyến nghị để chấm dứt một tiến trình: chúng có thể không đóng an toàn mọi tài nguyên đang mở hoặc không lưu được trạng thái chương trình cần thiết. Giải pháp tốt hơn là dùng cơ chế dừng có kiểm soát với cờ boolean an toàn cho tiến trình hoặc công cụ tương tự. 

Hướng dẫn đa xử lý trong Python

Giờ bạn đã hiểu những điều cơ bản về đa xử lý, hãy làm một ví dụ để minh họa cách lập trình đồng thời trong Python. 

Hàm chúng ta tạo sẽ đơn giản in ra một câu, ngủ 1 giây, rồi in ra câu khác - tìm hiểu thêm về hàm trong hướng dẫn hàm Python. 

import time

def do_something():
    print("I'm going to sleep")
    time.sleep(1)
    print("I'm awake") 

Bước đầu tiên là tạo tiến trình mới: chúng ta sẽ tạo hai tiến trình. 

# Create new child process

process_1 = Process(target=do_something)
process_2 = Process(target=do_something)

Đây là cách một chương trình đồng thời sẽ trông như thế nào trong môi trường notebook: 

%%time

# Starts both processes
process_1.start()
process_2.start()

"""
I'm going to sleep
CPU times: user 810 µs, sys: 7.34 ms, total: 8.15 ms
Wall time: 6.04 ms
I'm going to sleep
"""

Dựa trên đầu ra của chương trình, có thể thấy khá rõ là có vấn đề ở đâu đó trong mã của chúng ta. Bộ đếm thời gian được in giữa chừng tiến trình đầu tiên, và câu in thứ hai không được in. 

Điều này xảy ra vì có ba tiến trình đang chạy: tiến trình chính, và process_1 và process_2. Tiến trình theo dõi thời gian và in ra là tiến trình chính. Để tiến trình chính đợi trước khi in thời gian, chúng ta phải gọi phương thức join() trên hai tiến trình của mình sau khi chạy chúng.

Lưu ý: Nếu bạn quan tâm tìm hiểu thêm, hãy xem thảo luận trên Stackoverflow. 

Hãy xem đoạn mã mới của chúng ta: 

%%time
​
# Create new child process (Cannot run a process more than once)
new_process_1 = Process(target=do_something)
new_process_2 = Process(target=do_something)
​
# Starts both processes
new_process_1.start()
new_process_2.start()
​
new_process_1.join()
new_process_2.join()

"""
I'm going to sleep
I'm going to sleep
I'm awake
I'm awake
CPU times: user 0 ns, sys: 14 ms, total: 14 ms
Wall time: 1.01 s
"""

Vấn đề đã được giải quyết. 

Thời gian thực (wall time) của lần chạy này hơi dài hơn lần đầu. Tuy nhiên, lần này đã hoàn tất các lời gọi tới cả hai hàm đích trước khi trả về thông tin thời gian. Chúng ta cũng có thể áp dụng cùng lý luận này để cho nhiều tiến trình hơn chạy đồng thời. 

Kết luận về đa xử lý trong Python

Trong hướng dẫn này, bạn đã học cách làm cho chương trình Python hiệu quả hơn bằng cách chạy đồng thời. Cụ thể, bạn đã học: 

  • Tiến trình là gì và cách bạn có thể xem chúng trên máy tính của mình.
  • Những điểm tương đồng và khác biệt giữa mô-đun multiprocessing và threading của Python.
  • Những điều cơ bản của mô-đun multiprocessing và cách chạy chương trình Python đồng thời bằng multiprocessing. 

Muốn tìm hiểu thêm về Lập trình Python? Hãy xem lộ trình nghề nghiệp Python Programmer của chúng tôi. Không cần kinh nghiệm lập trình trước đó, và đến cuối lộ trình, bạn sẽ có được các kỹ năng cần thiết để phát triển phần mềm, xử lý dữ liệu và thực hiện phân tích nâng cao trong Python. 

Kurtis Pykes

Tác giả

Kurtis Pykes

Chủ đề

Liên quan