Hướng dẫn threading vs subprocess python - luồng so với quy trình con python
Bài 52 - Parallel Computing on Python30 Nov 2020 - phamdinhkhanh Show
1. Tại sao cần xử lý song songTrong quá trình xây dựng các ứng dụng deep learning trên python, mình nhận ra rằng để tạo ra một ứng dụng thì không khó. Nhưng để tạo ra một ứng dụng đáp ứng được tốc độ xử lý, độ chính xác và mức độ sử dụng resource thì cần phải tối ưu rất nhiều thứ. Bạn sẽ phải quan tâm đến các khía cạnh như:
Đặc biệt là các ứng dụng trên python thì tối ưu tốc độ xử lý là một challenge bởi python bị ràng buộc bởi cơ chế GIL (Global Interpreter Lock). Tức là nó chỉ cho phép một thread hoạt động truy suất và chỉnh sửa bộ nhớ tại một thời điểm. Do đó python không tận dụng được các tính toán đa luồng. Tuy nhiên ở python 3.2 trở đi thì python đã bắt đầu hỗ trợ đa luồng. Và thông qua bài viết này mình sẽ hướng dẫn các bạn có thể accelerate các ứng dụng của mình thông qua đa luồng. Nhưng trước tiên chúng ta cần hiểu về thread/process là gì? Vì blog dành cho đa dạng bạn đọc ở trình độ khác nhau nên bạn nào đã biết thì có thể bỏ qua phần kiến thức rất sơ đẳng này. 1.1. Thread và ProcessThread và process là hai khái niệm cơ bản trong lập trình và cũng có nhiều định nghĩa từ các nguồn khác nhau cho chúng. Process là gì? Chúng ta hiểu một cách đơn giản thì process là tiến trình để chạy một phần mềm. Khi bạn start một program thì tức là bạn đang khởi tạo một process. Hệ điều hành khi đó sẽ cung cấp các tài nguyên về memory, cpu, disk, bandwidth cho process để cho chạy ứng dụng của bạn. Hình 1: Khi bạn vào task management của window bạn có thể theo dõi các process đang chạy với mã PID của process. Mỗi một process sẽ phụ trách một instance của OS system và được cung cấp các thành phần như memory, cpu, disk, bandwith,….: Khi bạn vào task management của window bạn có thể theo dõi các process đang chạy với mã PID của process. Mỗi một process sẽ phụ trách một instance của OS system và được cung cấp các thành phần như memory, cpu, disk, bandwith,…. Lịch xử lý của các processes sẽ được OS sắp xếp dựa trên một số thuật toán lập lịch như round robin, first come first serve,… Mình sẽ không đi sâu vào phần này, các bạn có thể tham khảo thêm Operating System Scheduling algorithms. Threads là gì? Chắc hẳn bạn đã từng nghe đến thông số số cores của CPU. Các CPU càng hiện đại, số lượng cores sẽ càng nhiều. Các core sẽ hỗ trợ cho việc tính toán multi-task tốt hơn. Các threads sẽ được vận hành và tính toán trên các core của CPU. Một process khi được khởi tạo sẽ sinh ra các threads để run application. Bạn sẽ thắc mắc vậy thì chỉ cần một thread cũng được ? Tại sao lại cần nhiều threads? Nhiều threads sẽ giúp cho việc tính toán multi-task tốt hơn. Tức là bạn có thể làm nhiều nhiệm vụ một lúc. Nếu coi mỗi thread là một công nhân, thì việc sản xuất sẽ nhanh hơn nếu có nhiều công nhân phối hợp cùng làm việc. Bạn có thể hình dung dễ hơn qua ví dụ: Khi bạn làm việc với microsoft word, bạn gõ bàn phím thì có những công việc sau cần thực hiện:
Mỗi công việc được phụ trách bởi một thread và chúng phối hợp với nhau để giúp ứng dụng của bạn mượt hơn. Nếu chỉ có một thread làm tất cả mọi công việc thì nó sẽ bị quá tải và bạn có thể gặp phải giới hạn về tốc độ xử lý của CPU, thuật ngữ hay được gọi là CPU bound. Một process có thể là single-thread hoặc multiple-threads tùy thuộc vào số lượng là một hoặc nhiều. Khi có nhiều threads thì đòi hỏi phải có sự phối hợp tính toán song song (parrallel computing) giữa các threads với nhau. Từ đó sinh ra các khái niệm về đồng bộ ( import threading import time class FirstThread(threading.Thread): def __init__(self, thread_id, thread_name, counter): threading.Thread.__init__(self) self.thread_id = thread_id self.thread_name = thread_name self.counter = counter def run(self): print("Start thread {}!".format(self.thread_name)) while (self.counter): time.sleep(0.01) print("{} : {}".format(self.thread_name, self.counter)) self.counter -= 1 print("End thread {}".format(self.thread_name)) thread1 = FirstThread(1, "khanh thread", 5) thread2 = FirstThread(2, "ai thread", 5) thread1.start() thread2.start()4) và bất đồng bộ ( import threading import time class FirstThread(threading.Thread): def __init__(self, thread_id, thread_name, counter): threading.Thread.__init__(self) self.thread_id = thread_id self.thread_name = thread_name self.counter = counter def run(self): print("Start thread {}!".format(self.thread_name)) while (self.counter): time.sleep(0.01) print("{} : {}".format(self.thread_name, self.counter)) self.counter -= 1 print("End thread {}".format(self.thread_name)) thread1 = FirstThread(1, "khanh thread", 5) thread2 = FirstThread(2, "ai thread", 5) thread1.start() thread2.start()5). Chúng ta sẽ làm rõ hai khái niệm này ở các phần tiếp theo. Bạn có thể thắc mắc multiple-threads thì có khác gì khác biệt so với việc sử dụng multiple-processes? Chúng ta vẫn có thể tính toán song song được trên cả hai? Vậy tại sao lại cần phải tách một process thành nhiều threads làm gì ? Thực tế là trong python thì process và thread cùng kế thừa chung một interface là một base thread. Chúng sẽ có những đặc tính chung, nhưng thread là một phiên bản nhẹ hơn so với process. Do đó việc khởi tạo thread sẽ nhanh hơn. Một điểm khác biệt nữa đó là thread được thiết kế để có thể hoạt động tương tác lẫn nhau. Các threads trong cùng một process sẽ chia sẻ được dữ liệu qua lại nên có lợi thế về I/O. Dữ liệu của process thì được thiết kế private nên một process không thể chia sẻ dữ liệu với các process khác. Đây là lý do chúng ta cần nhiều threads hoạt động trong một process. Tiếp theo môi trường hoạt động của multiple-threads sẽ như thế nào ? Khi các threads chạy song song trên cùng một process, chúng sẽ khởi tạo dữ liệu như thế nào? Dữ liệu sẽ được lưu vào đâu? Chúng chia sẻ chung một code như thế nào? Chúng ta cùng làm rõ qua sơ đồ bên dưới. Hình 2: Cấu trúc của single thread và multiple threads. Cấu trúc của single thread và multiple threads. Đầu tiên ứng dụng của bạn sẽ khi khởi chạy sẽ load code lên. Phần main của chương trình sẽ được compiler khởi chạy đầu tiên. Lần lượt các method sẽ được load vào môi trường stack theo trình tự chạy. Compiler chạy lần lượt các hàm trong stack. Các hàm được compiler biên dịch thành mã máy ( import threading import time class FirstThread(threading.Thread): def __init__(self, thread_id, thread_name, counter): threading.Thread.__init__(self) self.thread_id = thread_id self.thread_name = thread_name self.counter = counter def run(self): print("Start thread {}!".format(self.thread_name)) while (self.counter): time.sleep(0.01) print("{} : {}".format(self.thread_name, self.counter)) self.counter -= 1 print("End thread {}".format(self.thread_name)) thread1 = FirstThread(1, "khanh thread", 5) thread2 = FirstThread(2, "ai thread", 5) thread1.start() thread2.start()6) và được thực thi để sinh ra dữ liệu. Dữ liệu sau đó được lưu trữ tại hai bộ nhớ là Heap và Stack (cái này cũng tùy thuộc vào virtual machine của từng ngôn ngữ). Stack lưu trữ method và các local variable còn heap lưu trữ object, array từ chương trình của bạn (phần lưu trữ này cũng có thể thay đổi tùy vào cách sắp xếp bộ nhớ của các ngôn ngữ). Nếu load trên stack thì không bị phân mảng dữ liệu và có thời gian load/access nhanh hơn. Còn heap sẽ allocate vùng nhớ ngẫu nhiên, các ô nhớ không liên tục nên do đó bị phân mảng. Okie, mình nghĩ lý thuyết như vậy là đủ rồi. Tiếp theo chúng ta sẽ cùng thực hành khởi tạo các thread và process trong python. 2. Khởi tạo thread trong python2.1. Khởi tạo từ hàmTrên python3 để khởi tạo một thread thì chúng ta sử dụng module import threading import time class FirstThread(threading.Thread): def __init__(self, thread_id, thread_name, counter): threading.Thread.__init__(self) self.thread_id = thread_id self.thread_name = thread_name self.counter = counter def run(self): print("Start thread {}!".format(self.thread_name)) while (self.counter): time.sleep(0.01) print("{} : {}".format(self.thread_name, self.counter)) self.counter -= 1 print("End thread {}".format(self.thread_name)) thread1 = FirstThread(1, "khanh thread", 5) thread2 = FirstThread(2, "ai thread", 5) thread1.start() thread2.start()7, trên python2 là import threading import time class FirstThread(threading.Thread): def __init__(self, thread_id, thread_name, counter): threading.Thread.__init__(self) self.thread_id = thread_id self.thread_name = thread_name self.counter = counter def run(self): print("Start thread {}!".format(self.thread_name)) while (self.counter): time.sleep(0.01) print("{} : {}".format(self.thread_name, self.counter)) self.counter -= 1 print("End thread {}".format(self.thread_name)) thread1 = FirstThread(1, "khanh thread", 5) thread2 = FirstThread(2, "ai thread", 5) thread1.start() thread2.start()8. Để start một method trên thread thì chúng ta chỉ cần truyền vào import threading import time class FirstThread(threading.Thread): def __init__(self, thread_id, thread_name, counter): threading.Thread.__init__(self) self.thread_id = thread_id self.thread_name = thread_name self.counter = counter def run(self): print("Start thread {}!".format(self.thread_name)) while (self.counter): time.sleep(0.01) print("{} : {}".format(self.thread_name, self.counter)) self.counter -= 1 print("End thread {}".format(self.thread_name)) thread1 = FirstThread(1, "khanh thread", 5) thread2 = FirstThread(2, "ai thread", 5) thread1.start() thread2.start()9 tên method và các giá trị đối số của nó. Ví dụ bên dưới chúng ta sử dụng hàm
|