Hướng dẫn python join all threads
Trong bài viết này, tôi muốn giới thiệu với các bạn về các cơ chế, kỹ thuật đồng bộ trong lập trình đa luồng (multithreading). Các kỹ thuật được trình bày trong ngôn ngữ Python nhưng về nguyên lý đều có thể áp dụng cho các ngôn ngữ khác. Những từ khóa chính trong bài viết: Show Giới thiệuTrước hết ta cần phân biệt khái niệm luồng (thread) và tiến trình (process). Thế nào là luồng? Thế nào là tiến trình? và sự khác nhau giữa chúng là gì? Hiểu đơn giản, Tiến trình (process) là một chương trình (program) đang được thực thi. Trong linux, ta có chương trình Luồng (thread) là một khối các câu lệnh (instructions) độc lập trong một tiến trình và có thể được lập lịch bởi hệ điều hành. Hay nói một cách đơn giản, Thread là các hàm hay thủ tục chạy độc lập đối với chương trình chính. Hình bên dưới minh họa sự khác nhau giữa luồng và tiến trình. nguồn: https://computing.llnl.gov/Một tiến trình có thể có nhiều luồng và phải có ít nhất một luồng, ta gọi đó là luồng chính (ví dụng hàm main() trong ngôn ngữ C). Ngoài các tài nguyên riêng của mình (các biến cục bộ trong hàm), các luồng chia sẻ tài nguyên chung của tiến trình. Việc thay đổi tài nguyên chung (ví dụ, đóng file, gán giá trị mới cho biến) từ một thread sẽ được nhìn thấy bởi tất cả các thread khác. Vì vậy, lập trình viên cần phải thực hiện đồng bộ việc truy cập tài nguyên chung giữa các luồng. Tại sao cần phải đồng bộTrong lập trình đa luồng, các threads chia sẻ chung tài nguyên của tiến trình, vì vậy có những thời điểm nhiều luồng sẽ đồng thời thay đổi dữ liệu chung, dẫn đến dữ liệu chung sẽ bị corrupt, sẽ cho ra kết qủa không mong muốn. Do đó, ta cần những cơ chể để đảm bảo rằng, tại một thời điểm chỉ có duy nhất một luồng được phép truy cập vào dữ liệu chung, nếu các luồng khác muốn truy cập vào đoạn dữ liệu này thì cần phải đợi cho thread trước đó hoàn thành công việc của mình. Ta gọi đấy là đồng bộ trong lập trình đa luồng. Giả sử ta muốn xây dựng một ứng dụng web có lưu số lượng truy cập vào một biết toàn cục
Trong trường hợp trên, lẽ ra giá trị của Để giải quyết vấn đề này, ta cần phải đảm bảo rằng tại một thời điểm chỉ có một thread được quyền truy cập dữ liệu chung, điều này đảm bảo rằng kết quả của chương trình đa luồng phải chính xác như khi chương trình đó chạy tuần tự đơn luồng. Lập trình đa luồng trong PythonModule threading của thư viện chuẩn Python cung cấp cho chúng ta các class và function để làm việc với thread, nó cũng cung cấp các cơ chế để đồng bộ luồng, bao gồm: Thread, Lock, RLock, Condition, Semaphore,Event,... Note: Trong Cpython, Global Interpreter Lock - GIL giới hạn chỉ có một thread được chạy tại một thời điểm, nên về cơ bản Cpython sẽ không hoàn toàn hỗ trợ đa luồng, nhưng ta có thể sử dụng các Interpreter khác (Jython và IronPython) không sử dụng GIL vì vậy có thể chạy đa luồng. Class Thread cung cấp cho chúng ta các phương thức cần thiết để làm việc với luồng, cụ thể ta có:
Để tạo một luồng mới với class Thread, ta có 2 cách:
Đoạn code dưới đây minh họa 2 cách tạo create như trên:
Các cơ chế đồng bộ trong PythonTrong phần này, tôi sẽ trình bày về các cơ chế đồng bộ trong lập trình đa luồng thông qua bài toán đếm số nguyên tố: Cho số nguyên dương N, liệt kê ra các số nguyên tố trong đoạn từ 2 đến N. Nếu giải bài toán này theo cách tuần tự (đơn luồng) thông thường thật đơn giản. Chỉ cần cài đặt hàm kiểm tra một số đầu vào có là số nguyên tố hay không, sau đó kiểm tra từng số trong đoạn từ 2 đến N, nếu là số nguyên tố thì in ra màn hình. Trong lập trình đa luồng, có 3 yếu tố ta cần phải quan tâm đó là:
Chia nhỏ bài toán: Trong bài toán trên, ta có 2 cách để chia nhỏ bài toán:
Về cân bằng tải giữa luồng: Ta dễ thấy rằng nếu số càng lớn thì khối lượng tính toán để kiểm tra xem số đó có là số nguyên tố hay không càng lớn, vì vậy với cách chia thứ nhất, những luồng xử lý trên dãy các số nguyên bé sẽ hoàn thành công việc sớm hơn, và những luồng về sau sẽ càng mất nhiều thời gian để tính toán hơn, cho dù số phần tử của mỗi đoạn là bằng nhau. Do đó, ta thấy rằng cách chia thứ nhất cân bằng tải giữa các luồng không được tốt. Với cách chia thứ hai, các luồng sẽ luân phiên nhau lấy ra một gía trị trong mảng chung, kiểm tra tính nguyên tố của gía trị đó một cách độc lập, sau khi hoàn thành công việc sẽ lấy một gía trị khác để kiểm tra, ta thấy rằng công việc được phân bố đều giữa các luồng, vì vậy cách chia thứ hai cho ta cân bằng tải tốt hơn. Về đồng bộ luồng: Ở đây ta xét cách chia thứ hai, các luồng sẽ chia sẻ 2 tài nguyên chung đó là mảng các số 2 -> N và mảng chứa các số nguyên tố. Ta cần đảm bảo tại mỗi thời điểm chỉ có duy nhất một luồng được truy cập tài nguyên chung, nếu luồng khác muốn truy cập phải đợi (block) cho đến khi luồng trước đó cập nhật
xong. Python cung cấp cho chúng ta các class cơ bản để thực hiện đồng bộ luồng bao gồm: LockLock là cơ chế đồng bộ cơ bản nhất của Python. Một Lock gồm có 2 trạng thái, locked và unlocked, cùng với 2 phương thức để thao tác với Lock là
Tại mỗi thời điểm, chỉ có nhiều nhất một thread sở hữu Lock. Với cơ chế Lock, ta có thể đồng bộ bài toán đếm số nguyên tố như sau:
Rlock (Reentrant Lock)Tương tự như Lock và sử dụng khái niệm "owning thread" (tiến trình sở hữu) và "recursion level" (có thể gọi method acquire() nhiều lần).
Bởi vì đặc điểm trên, Rlock có thể hữu ích cho các hàm đệ quy:
Hoặc trong những trường hợp ta muốn duy trình mối quan hệ sở hữu, ta muốn lock chỉ được giải phóng (release) với thread sở hữu nó. ConditionĐây là cơ chế đồng bộ được sử dụng khi một thread muốn đợi (block) một điều kiện nào đấy xảy ra và một thread khác sẽ thông báo (notify) khi điều kiện đã xảy ra, lúc đấy thread đang đợi sẽ được đánh thức và dành được khóa để truy cập độc quyền vào tài nguyên chung (không có thread nào khác được phép truy cập vào tài nguyên khi thread này chưa realse khóa). Biến điều kiện luôn liên kết với một khóa bên dưới (Lock hoặc RLock). Biến điều kiện cung cấp các method chính như sau:
Một ví dụ minh họa cực hay cho cơ chế đồng bộ này đó là bài toán producer/cosumer (sản-xuât/tiêu-thụ). Thread producer thêm (sản-xuất) một số nguyên ngẫu nhiên tới một mảng chung tại những thời điểm ngẫu nhiên và thread consumber lấy (tiêu-thụ) ra những số nguyên từ mảng chung đó. Trong đoạn code bên dưới, Producer object dành (acquire) được khóa, thêm một số nguyên vào mảng, thông báo (notify) cho Consumber thread rằng đã có dữ liệu (integer) để sử dụng và giải phóng (release) khóa.
Tiếp đến là Consumer object, nó dành được khóa, kiểm tra xem có phần tử nào trong mảng không. Nếu mảng rỗng, nó đợi (wait) cho đến khi được thông báo (notify) bởi producer. Một khi phần tử được lấy về từ mảng chung, consumer giải phóng khóa. Lưu ý rằng lời gọi wait() giải phóng khóa vì vậy producer có thể dành được tài nguyên và thực hiện công việc của mình.
Đoạn code dưới đây ta sẽ cài đặt hàm main, tạo ra 2 thread consumer và producer (file condition.py):
Đầu ra khi chạy chương trình:
SemaphoreĐây là một trong những cơ chế đồng bộ lâu đời nhất trong lịch sử khoa học máy tính, được phát minh bởi nhà khoa học máy tính Edsger W. Dijkstra (trong đó ông sử dụng P() và V() thay vì acquire() và release()). Một Semaphore duy trì một biến đếm (không âm) được truyền vào khi khởi tạo một Semaphore object, có giá trị giảm sau mỗi lần gọi acquire() và tăng sau mõi lần gọi method release(). Khi gọi method acquire() trên một Semaphore object với giá trị biến đếm bằng 0, nó sẽ block thread gọi và đợi cho đến khi thread khác gọi release() (làm tăng giá trị biến đếm lên 1). Semaphore cũng cung cấp 2 method là Với cơ chế như trên, Semaphore thường được dùng để giới hạn số lượng thread đồng thời truy cập vào tài nguyên chúng, ví dụ, giới hạn số lượng kết nối tới một database server. Dễ nhận thấy rằng Lock là một trường hợp riêng của Semaphore trong đó biến đếm được khởi tạo bằng 1.
EventEvent là cơ chế đồng bộ đơn giản trong đó một thread sẽ phát ra một sự kiện (event) và các thread khác đợi sự kiện đó. Một Event object quản lý một cờ trong (internal flag), được thiết lập True với method Quay trở lại với bài toán producer/consumer ở trên, ta sẽ cài đặt nó sử dụng Event thay vì Condition. Mỗi lần một số nguyên được thêm vào mảng chung, sự kiện sẽ được thiết lập (set) và cờ sẽ được clear ngay sau đó để thông báo tới consumer. Lưu ý rằng cờ được clear (False) khi khởi tạo Event object.
Tiếp đến là Consumer class, ta cũng truyền một Event object cho hàm khởi tạo. Thread consumer sẽ bị block (khi gọi method wait()) cho đến khi sự kiện được phát ra, cho thấy rằng mảng chung đã có dữ liệu.
Dưới đây ta sẽ cài đặt hàm main để thử nghiệm (file event.py):
Đầu ra của đoạn chương trình:
Kết luậnPhuuu, bạn vẫn đọc đến đây đấy chứ. Hy vọng bài viết hữu ích cho những ai đang tìm hiểu các cơ chế đồng bộ khi lập trình đa luồng trong Python. Bài viết rất mong nhận được sự góp ý cũng như trao đổi từ các bạn. Thank you! |