Đa luồng trong Python với ví dụ: Tìm hiểu GIL trong Python
⚡ Tóm tắt thông minh
Đa luồng trong Python Phương pháp này chạy nhiều luồng bên trong một tiến trình duy nhất, do đó chúng chia sẻ bộ nhớ và hoạt động đồng thời. Mô-đun luồng tạo và quản lý các luồng này, trong khi Khóa Trình thông dịch Toàn cục giới hạn khả năng song song thực sự, khiến kỹ thuật này phù hợp nhất cho các tác vụ phụ thuộc vào đầu vào/đầu ra.

Python ngôn ngữ lập trình cho phép bạn sử dụng đa xử lý hoặc đa luồng. Trong hướng dẫn này, bạn sẽ học cách viết các ứng dụng đa luồng trong Python.
Chủ đề là gì?
Luồng (thread) là một đơn vị thực thi trong lập trình song song. Đa luồng là một kỹ thuật cho phép CPU thực thi nhiều tác vụ của một tiến trình cùng một lúc. Các luồng này có thể thực thi độc lập trong khi vẫn chia sẻ tài nguyên của tiến trình.
Một quy trình là gì?
Về cơ bản, tiến trình là chương trình đang được thực thi. Khi bạn khởi chạy một ứng dụng trên máy tính (như trình duyệt hoặc trình soạn thảo văn bản), hệ điều hành sẽ tạo ra một tiến trình. quá trình.
Đa luồng trong Python?
Đa luồng trong Python Lập trình đa luồng là một kỹ thuật nổi tiếng, trong đó nhiều luồng trong một tiến trình chia sẻ không gian dữ liệu với luồng chính, giúp việc chia sẻ thông tin và giao tiếp giữa các luồng trở nên dễ dàng và hiệu quả. Luồng nhẹ hơn tiến trình. Nhiều luồng có thể thực thi độc lập trong khi vẫn chia sẻ tài nguyên của tiến trình. Mục đích của đa luồng là để chạy nhiều tác vụ và chức năng cùng một lúc.
Đa xử lý là gì?
Đa xử lý cho phép bạn chạy nhiều tiến trình không liên quan cùng lúc. Các tiến trình này không chia sẻ tài nguyên của chúng và giao tiếp thông qua IPC.
Python Đa luồng vs Đa xử lý
Để hiểu về tiến trình và luồng, hãy xem xét tình huống này: Một tệp .exe trên máy tính của bạn là một chương trình. Khi bạn mở nó, hệ điều hành sẽ tải nó vào bộ nhớ và CPU sẽ thực thi nó. Phiên bản của chương trình đang chạy được gọi là tiến trình.
Mỗi quy trình đều có hai thành phần cơ bản:
- Code
- Dữ liệu
Bây giờ, một tiến trình có thể chứa một hoặc nhiều phần con được gọi là chủ đề. Điều này phụ thuộc vào kiến trúc hệ điều hành. Bạn có thể coi luồng (thread) như một phần của tiến trình có thể được hệ điều hành thực thi riêng biệt.
Nói cách khác, đó là một chuỗi các lệnh có thể được hệ điều hành thực thi độc lập. Các luồng trong cùng một tiến trình chia sẻ dữ liệu của tiến trình đó và được thiết kế để hoạt động cùng nhau nhằm tạo điều kiện thuận lợi cho tính song song.
Tại sao nên sử dụng Đa luồng?
Đa luồng cho phép bạn chia nhỏ ứng dụng thành nhiều tác vụ phụ và chạy các tác vụ này cùng lúc. Nếu bạn sử dụng đa luồng đúng cách, tốc độ, hiệu suất và khả năng hiển thị của ứng dụng đều có thể được cải thiện.
Python Đa luồng
Python Hỗ trợ các cấu trúc cho cả xử lý đa tiến trình và đa luồng. Trong hướng dẫn này, bạn sẽ tập trung chủ yếu vào việc triển khai đa luồng ứng dụng với PythonCó hai mô-đun chính có thể được sử dụng để xử lý các luồng trong đó. Python:
- Chủ đề mô-đun, và
- luồng mô-đun
Tuy nhiên, trong PythonNgoài ra còn có một khái niệm gọi là khóa trình thông dịch toàn cục (GIL). Tuy nhiên, nó không mang lại nhiều lợi ích về hiệu năng và thậm chí có thể còn không hiệu quả. giảm hiệu suất của một số ứng dụng đa luồng. Bạn sẽ tìm hiểu tất cả về nó trong các phần sắp tới của hướng dẫn này.
Các mô-đun Thread và Threading
Hai mô-đun mà bạn sẽ tìm hiểu trong hướng dẫn này là mô-đun chủ đề và mô-đun luồng.
Tuy nhiên, mô-đun luồng từ lâu đã không còn được dùng nữa. Bắt đầu với Python 3, nó đã được coi là lỗi thời và chỉ có thể truy cập được dưới dạng _chủ đề để tương thích ngược.
Bạn nên sử dụng cấp độ cao hơn luồng Đây là mô-đun dành cho các ứng dụng mà bạn dự định triển khai. Mô-đun luồng chỉ được đề cập ở đây nhằm mục đích giáo dục.
Mô-đun chủ đề
Cú pháp để tạo một chủ đề mới bằng mô-đun này như sau:
thread.start_new_thread(function_name, arguments)
Được rồi, bây giờ bạn đã nắm được lý thuyết cơ bản để bắt đầu viết mã. Vì vậy, hãy mở IDLE hoặc một cuốn sổ tay và nhập nội dung sau:
import time import _thread def thread_test(name, wait): i = 0 while i <= 3: time.sleep(wait) print("Running %s\n" %name) i = i + 1 print("%s has finished execution" %name) if __name__ == "__main__": _thread.start_new_thread(thread_test, ("First Thread", 1)) _thread.start_new_thread(thread_test, ("Second Thread", 2)) _thread.start_new_thread(thread_test, ("Third Thread", 3))
Lưu file và nhấn F5 để chạy chương trình. Nếu mọi thứ đã được thực hiện chính xác thì đây là kết quả mà bạn sẽ thấy:
Bạn sẽ tìm hiểu thêm về điều kiện thi đấu và cách xử lý chúng trong các phần tiếp theo.
GIẢI THÍCH MÃ
- Các câu lệnh này nhập mô-đun thời gian và luồng, được sử dụng để xử lý việc thực thi và trì hoãn. Python chủ đề.
- Ở đây, bạn đã xác định một hàm gọi là thread_test, cái này sẽ được gọi bởi bắt đầu_new_thread Phương thức này chạy một vòng lặp while trong bốn lần và in ra tên của luồng đã gọi nó. Sau khi vòng lặp hoàn tất, nó in ra một thông báo cho biết luồng đã kết thúc quá trình thực thi.
- Đây là phần chính của chương trình của bạn. Ở đây, bạn chỉ cần gọi bắt đầu_new_thread phương pháp với thread_test Truyền một hàm làm đối số. Thao tác này sẽ tạo một luồng mới cho hàm bạn truyền làm đối số và bắt đầu thực thi nó. Lưu ý rằng bạn có thể thay thế (thread_test) bằng bất kỳ hàm nào khác mà bạn muốn chạy dưới dạng một luồng.
Mô-đun luồng
Mô-đun này là sự triển khai cấp cao của việc phân luồng trong Python và tiêu chuẩn thực tế để quản lý các ứng dụng đa luồng. Nó cung cấp một loạt các tính năng khi so sánh với mô-đun luồng.
Cấu trúc của module Threading
Dưới đây là danh sách một số chức năng hữu ích được xác định trong mô-đun này:
| Tên chức năng | Mô tả Chi tiết |
|---|---|
| hoạt độngCount() | Trả về số lượng Sợi chỉ Những vật thể vẫn còn sống. |
| currentThread() | Trả về đối tượng hiện tại của lớp Thread. |
| liệt kê () | Liệt kê tất cả các đối tượng Thread đang hoạt động. |
| isDaemon() | Trả về true nếu luồng là daemon. |
| isAlive () | Trả về true nếu thread vẫn còn tồn tại. |
| Các phương thức lớp chủ đề | |
| khởi đầu() | Bắt đầu hoạt động của một chủ đề. Nó chỉ được gọi một lần cho mỗi luồng vì nó sẽ gây ra lỗi thời gian chạy nếu được gọi nhiều lần. |
| chạy() | Phương thức này biểu thị hoạt động của một luồng và có thể bị ghi đè bởi một lớp mở rộng lớp Thread. |
| tham gia() | Nó chặn việc thực thi mã khác cho đến khi luồng mà phương thức join() được gọi trên đó bị chấm dứt. |
Cốt truyện: Lớp chủ đề
Trước khi bắt đầu lập trình đa luồng bằng cách sử dụng mô-đun threading, điều quan trọng là phải hiểu lớp Thread. Lớp Thread là lớp chính định nghĩa khuôn mẫu và các thao tác của một luồng trong lập trình. Python.
Cách phổ biến nhất để tạo một ứng dụng đa luồng Python Ứng dụng này nhằm mục đích khai báo một lớp kế thừa từ lớp Thread và ghi đè phương thức run() của lớp đó.
Tóm lại, lớp Thread biểu thị một chuỗi mã chạy trong một Chủ đề kiểm soát.
Vì vậy, khi viết một ứng dụng đa luồng, bạn sẽ thực hiện như sau:
- Định nghĩa một lớp kế thừa từ lớp Thread.
- Ghi đè __trong đó__ constructor
- Ghi đè chạy() phương pháp
Khi một đối tượng luồng đã được tạo, khởi đầu() phương pháp này có thể được sử dụng để bắt đầu thực hiện hoạt động này, và tham gia() phương pháp này có thể được sử dụng để chặn tất cả các mã khác cho đến khi hoạt động hiện tại kết thúc.
Bây giờ, chúng ta hãy thử sử dụng mô-đun luồng để triển khai ví dụ trước đó của bạn. Một lần nữa, hãy khởi động lại chương trình của bạn. IDLE và nhập nội dung sau:
import time import threading class threadtester (threading.Thread): def __init__(self, id, name, i): threading.Thread.__init__(self) self.id = id self.name = name self.i = i def run(self): thread_test(self.name, self.i, 5) print ("%s has finished execution " %self.name) def thread_test(name, wait, i): while i: time.sleep(wait) print ("Running %s \n" %name) i = i - 1 if __name__=="__main__": thread1 = threadtester(1, "First Thread", 1) thread2 = threadtester(2, "Second Thread", 2) thread3 = threadtester(3, "Third Thread", 3) thread1.start() thread2.start() thread3.start() thread1.join() thread2.join() thread3.join()
Đây sẽ là đầu ra khi bạn thực thi đoạn mã trên:
GIẢI THÍCH MÃ
- Phần này tương tự như ví dụ trước. Tại đây, bạn nhập mô-đun thời gian và luồng, được sử dụng để xử lý việc thực thi và độ trễ của... Python chủ đề.
- Trong phần này, bạn đang tạo một lớp có tên là threadtester, lớp này kế thừa hoặc mở rộng Sợi chỉ lớp của mô-đun luồng. Đây là một trong những cách phổ biến nhất để tạo chủ đề trong Python. Tuy nhiên, bạn chỉ nên ghi đè hàm tạo và chạy() phương pháp trong ứng dụng của bạn. Như bạn có thể thấy trong mẫu mã ở trên, __trong đó__ phương thức (hàm tạo) đã bị ghi đè. Tương tự, bạn cũng đã ghi đè chạy() phương pháp. Nó chứa mã mà bạn muốn thực thi bên trong một luồng. Trong ví dụ này, bạn đã gọi hàm thread_test().
- Đây là phương thức thread_test(), phương thức này nhận giá trị là i Biến i được truyền vào làm đối số, giảm giá trị của nó đi 1 ở mỗi lần lặp và lặp lại phần còn lại của mã cho đến khi i bằng 0. Ở mỗi lần lặp, nó in ra tên của luồng đang thực thi và tạm dừng trong wait giây (thời gian này cũng được truyền vào làm đối số).
- thread1 = threadtester(1, “First Thread”, 1) Ở đây, chúng ta đang tạo một luồng và truyền ba tham số mà chúng ta đã khai báo trong __init__. Tham số đầu tiên là id của luồng, tham số thứ hai là tên của luồng và tham số thứ ba là bộ đếm, xác định số lần vòng lặp while sẽ chạy.
- Phương thức `thread2.start()` được sử dụng để bắt đầu thực thi một luồng. Bên trong, hàm `start()` gọi phương thức `run()` của lớp bạn đang xử lý.
- thread3.join() Phương thức join() chặn việc thực thi mã khác và đợi cho đến khi luồng mà nó được gọi kết thúc.
Như bạn đã biết, các luồng trong cùng một tiến trình có quyền truy cập vào bộ nhớ và dữ liệu của tiến trình đó. Do đó, nếu nhiều hơn một luồng cố gắng thay đổi hoặc truy cập dữ liệu cùng lúc, lỗi có thể xảy ra.
Trong phần tiếp theo, bạn sẽ thấy các loại sự cố khác nhau có thể xảy ra khi các luồng truy cập dữ liệu và vùng quan trọng mà không kiểm tra các giao dịch truy cập hiện có.
Bế tắc và điều kiện chạy đua
Trước khi tìm hiểu về tắc nghẽn và điều kiện tranh chấp, việc hiểu một vài định nghĩa cơ bản liên quan đến lập trình song song sẽ rất hữu ích:
- Phần quan trọng: Đó là một đoạn mã truy cập hoặc sửa đổi các biến dùng chung và phải được thực hiện như một giao dịch nguyên tử.
- Chuyển đổi ngữ cảnh: Đó là quá trình mà CPU thực hiện để lưu trữ trạng thái của một luồng trước khi chuyển từ tác vụ này sang tác vụ khác, để sau đó có thể tiếp tục từ cùng một điểm.
Bế tắc
Bế tắc là vấn đề đáng sợ nhất mà các nhà phát triển phải đối mặt khi viết các ứng dụng đồng thời/đa luồng trong PythonCách tốt nhất để hiểu về tình trạng tắc nghẽn là sử dụng ví dụ bài toán kinh điển trong khoa học máy tính, được gọi là... Nhà Hàng PhiloVấn đề sophers.
Câu hỏi đặt ra cho các triết gia ăn uống như sau:
Năm nhà triết học đang ngồi quanh một chiếc bàn tròn với năm đĩa mì Ý (một loại mì ống) và năm chiếc nĩa, như thể hiện trong sơ đồ.
Nhà Hàng Philovấn đề sophers
Vào bất kỳ thời điểm nào, một triết gia phải đang ăn hoặc đang suy nghĩ.
Hơn nữa, một triết gia phải lấy hai chiếc nĩa gần mình (tức là nĩa trái và nĩa phải) trước khi có thể ăn mì spaghetti. Vấn đề bế tắc xảy ra khi cả năm triết gia cùng cầm nĩa phải của mình lên cùng một lúc.
Vì mỗi triết gia đều có một chiếc nĩa, nên họ sẽ đợi những người khác đặt nĩa xuống. Kết quả là không ai trong số họ có thể ăn được mì spaghetti.
Tương tự như vậy, trong một hệ thống đồng thời, bế tắc xảy ra khi các luồng hoặc quy trình khác nhau (philosophers) cố gắng có được các tài nguyên hệ thống được chia sẻ (fork) cùng một lúc. Kết quả là, không có quy trình nào có cơ hội thực thi vì chúng đang chờ một tài nguyên khác do một quy trình khác nắm giữ.
Điều kiện cuộc đua
Tình trạng tranh chấp tài nguyên (race condition) là một trạng thái không mong muốn của chương trình xảy ra khi hệ thống thực hiện hai hoặc nhiều thao tác đồng thời. Ví dụ, hãy xem xét vòng lặp for đơn giản này:
i=0; # a global variable for x in range(100): print(i) i+=1;
Nếu bạn tạo n Với số lượng luồng chạy đoạn mã này cùng một lúc, bạn không thể xác định giá trị của i (được chia sẻ bởi các luồng) khi chương trình kết thúc thực thi. Điều này là do trong môi trường đa luồng thực tế, các luồng có thể chồng chéo nhau, và giá trị của i được một luồng truy xuất và sửa đổi có thể thay đổi trong quá trình một luồng khác truy cập vào nó.
Đây là hai loại vấn đề chính có thể xảy ra trong môi trường đa luồng hoặc phân tán. Python ứng dụng. Trong phần tiếp theo, bạn sẽ học cách khắc phục vấn đề này bằng cách đồng bộ hóa các luồng.
Syncchủ đề đồng bộ hóa
Để giải quyết các tình trạng chạy đua, bế tắc và các vấn đề dựa trên luồng khác, mô-đun luồng cung cấp Khóa object. Ý tưởng là khi một luồng muốn truy cập vào một tài nguyên cụ thể, nó sẽ có được một khóa cho tài nguyên đó. Khi một luồng khóa một tài nguyên cụ thể, không luồng nào khác có thể truy cập vào tài nguyên đó cho đến khi khóa được giải phóng. Kết quả là, các thay đổi đối với tài nguyên sẽ là nguyên tử và các điều kiện chạy đua sẽ được ngăn chặn.
Khóa là một nguyên thủy đồng bộ hóa cấp thấp được triển khai bởi _chủ đề mô-đun. Tại bất kỳ thời điểm nào, một khóa có thể ở một trong hai trạng thái: khóa or đã mở khóa. Nó hỗ trợ hai phương pháp:
- giành được(): Khi trạng thái khóa được mở, việc gọi phương thức acquire() sẽ thay đổi trạng thái thành locked và trả về. Tuy nhiên, nếu trạng thái đang là locked, lệnh gọi acquire() sẽ bị chặn cho đến khi phương thức release() được gọi bởi một luồng khác.
- giải phóng(): Phương thức phát hành () được sử dụng để đặt trạng thái thành đã mở khóa, tức là để giải phóng khóa. Nó có thể được gọi bởi bất kỳ luồng nào, không nhất thiết phải là luồng đã lấy được khóa.
Đây là một ví dụ về cách sử dụng khóa trong ứng dụng của bạn. Khởi động ứng dụng của bạn! IDLE và nhập nội dung sau:
import threading lock = threading.Lock() def first_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the first funcion') lock.release() def second_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the second funcion') lock.release() if __name__=="__main__": thread_one = threading.Thread(target=first_function) thread_two = threading.Thread(target=second_function) thread_one.start() thread_two.start() thread_one.join() thread_two.join()
Bây giờ, nhấn F5. Bạn sẽ thấy một kết quả như thế này:
GIẢI THÍCH MÃ
- Ở đây, bạn chỉ cần tạo một khóa mới bằng cách gọi luồng.Lock() chức năng của nhà máy. Trong nội bộ, Lock() trả về một phiên bản của lớp Lock cụ thể hiệu quả nhất được nền tảng duy trì.
- Trong câu lệnh đầu tiên, bạn lấy được khóa bằng cách gọi phương thức thu được(). Khi khóa đã được cấp, bạn in “đã lấy được khóa” đến bảng điều khiển. Khi tất cả mã mà bạn muốn luồng chạy đã thực thi xong, bạn giải phóng khóa bằng cách gọi phương thức Release().
Về lý thuyết thì ổn, nhưng làm sao bạn biết chắc chắn rằng khóa đã hoạt động? Nếu bạn nhìn vào kết quả đầu ra, bạn sẽ thấy rằng mỗi câu lệnh print chỉ in ra đúng một dòng tại một thời điểm. Hãy nhớ lại ví dụ trước đó, kết quả đầu ra từ hàm print bị xáo trộn vì nhiều luồng truy cập phương thức print() cùng một lúc. Ở đây, hàm print chỉ được gọi sau khi khóa đã được giành quyền. Vì vậy, kết quả đầu ra được hiển thị từng dòng một.
Ngoài ổ khóa, Python Ngoài ra, hệ thống còn hỗ trợ một số cơ chế khác để xử lý đồng bộ hóa luồng, như được liệt kê bên dưới:
- RKhóa
- Semaphores
- Điều kiện
- Sự kiện và
- Rào cản
Khóa phiên dịch toàn cầu (và cách xử lý nó)
Trước khi đi vào chi tiết của PythonGIL thân mến, chúng ta hãy cùng định nghĩa một vài thuật ngữ sẽ hữu ích trong việc hiểu phần tiếp theo:
- Mã phụ thuộc vào CPU: thuật ngữ này đề cập đến bất kỳ đoạn mã nào sẽ được CPU thực thi trực tiếp.
- Mã phụ thuộc vào I/O: đây có thể là bất kỳ đoạn mã nào truy cập hệ thống tập tin thông qua hệ điều hành.
- CPython: đó là tài liệu tham khảo thực hiện of Python và có thể được mô tả như trình thông dịch được viết bằng C và Python (ngôn ngữ lập trình).
GIL trong đó là gì Python?
Khóa thông dịch viên toàn cầu (GIL) in Python GIL (Global Interpreter Lock) là một loại khóa tiến trình hoặc mutex được sử dụng khi làm việc với các tiến trình. Nó đảm bảo rằng chỉ một luồng có thể truy cập vào một tài nguyên cụ thể tại một thời điểm, và nó cũng ngăn chặn việc sử dụng đồng thời các đối tượng và mã bytecode. Điều này mang lại lợi ích cho các chương trình đơn luồng bằng cách tăng hiệu suất. GIL trong... Python rất đơn giản và dễ thực hiện.
Khóa có thể được sử dụng để đảm bảo rằng chỉ có một luồng có quyền truy cập vào một tài nguyên cụ thể tại một thời điểm nhất định.
Một trong những tính năng của Python là nó sử dụng khóa chung trên mỗi quy trình thông dịch, có nghĩa là mọi quy trình đều xử lý Python chính trình thông dịch như một nguồn tài nguyên.
Ví dụ, giả sử bạn đã viết một Python Chương trình này sử dụng hai luồng để thực hiện cả các thao tác CPU và 'I/O'. Khi bạn chạy chương trình này, đây là những gì sẽ xảy ra:
- Python Trình thông dịch tạo ra một tiến trình mới và sinh ra các luồng.
- Khi thread-1 bắt đầu chạy, trước tiên nó sẽ lấy GIL và khóa nó.
- Nếu thread-2 muốn thực thi ngay bây giờ, nó sẽ phải đợi GIL được giải phóng ngay cả khi bộ xử lý khác rảnh.
- Bây giờ, giả sử luồng-1 đang chờ thao tác I/O. Tại thời điểm này, nó sẽ giải phóng GIL và thread-2 sẽ thu được nó.
- Sau khi hoàn thành các thao tác I/O, nếu luồng-1 muốn thực thi ngay bây giờ, nó sẽ lại phải đợi GIL được giải phóng bởi luồng-2.
Do đó, chỉ có một luồng có thể truy cập trình thông dịch bất cứ lúc nào, nghĩa là sẽ chỉ có một luồng thực thi Python Mã tại một thời điểm nhất định.
Điều này ổn đối với bộ xử lý đơn nhân vì nó sẽ sử dụng phương pháp phân chia thời gian (xem phần đầu tiên của hướng dẫn này) để xử lý các luồng. Tuy nhiên, trong trường hợp bộ xử lý đa nhân, một hàm phụ thuộc vào CPU thực thi trên nhiều luồng sẽ ảnh hưởng đáng kể đến hiệu suất của chương trình vì nó sẽ không thực sự sử dụng tất cả các lõi có sẵn cùng một lúc.
Tại sao cần GIL?
CPython Trình thu gom rác sử dụng một kỹ thuật quản lý bộ nhớ hiệu quả được gọi là đếm tham chiếu. Cách thức hoạt động như sau: Mỗi đối tượng trong Python Một đối tượng có số lượng tham chiếu, số lượng này tăng lên khi nó được gán cho một tên biến mới hoặc được thêm vào một vùng chứa (như bộ dữ liệu, danh sách, v.v.). Tương tự, số lượng tham chiếu giảm đi khi tham chiếu nằm ngoài phạm vi hoặc khi câu lệnh `del` được gọi. Khi số lượng tham chiếu của một đối tượng đạt đến 0, nó sẽ được thu gom rác và bộ nhớ được cấp phát sẽ được giải phóng.
Nhưng vấn đề là biến số tham chiếu dễ bị ảnh hưởng bởi các điều kiện giống như bất kỳ biến toàn cục nào khác. Để giải quyết vấn đề này, các nhà phát triển Python Chúng tôi quyết định sử dụng khóa trình thông dịch toàn cục. Lựa chọn khác là thêm khóa vào từng đối tượng, điều này sẽ dẫn đến tình trạng bế tắc và tăng chi phí từ các lệnh acquire() và release().
Vì vậy, GIL là một hạn chế đáng kể cho đa luồng Python các chương trình chạy các tác vụ nặng về CPU (thực chất là các chương trình đơn luồng). Nếu bạn muốn sử dụng nhiều lõi CPU trong ứng dụng của mình, hãy sử dụng đa xử lý mô-đun thay thế.








