Chủ đề liên lạc
Trong cuộc sống thực, nếu một nhóm người đang thực hiện một nhiệm vụ chung thì cần có sự giao tiếp giữa họ để hoàn thành nhiệm vụ đúng cách. Tương tự như vậy cũng có thể áp dụng cho các chủ đề. Trong lập trình, để giảm thời gian lý tưởng của bộ xử lý chúng ta tạo nhiều luồng và gán các nhiệm vụ con khác nhau cho mỗi luồng. Do đó, cần phải có một phương tiện liên lạc và chúng phải tương tác với nhau để hoàn thành công việc một cách đồng bộ.
Hãy xem xét các điểm quan trọng sau liên quan đến thông tin liên lạc theo chuỗi:
No performance gain - Nếu chúng ta không thể đạt được giao tiếp thích hợp giữa các luồng và các quy trình thì hiệu suất đạt được từ sự đồng thời và song song sẽ không có ích gì.
Accomplish task properly - Nếu không có cơ chế liên lạc thích hợp giữa các luồng thì không thể hoàn thành nhiệm vụ được giao.
More efficient than inter-process communication - Giao tiếp giữa các luồng hiệu quả hơn và dễ sử dụng hơn so với giao tiếp giữa các tiến trình vì tất cả các luồng trong một tiến trình chia sẻ cùng một không gian địa chỉ và chúng không cần sử dụng bộ nhớ dùng chung.
Cấu trúc dữ liệu Python cho giao tiếp an toàn theo luồng
Mã đa luồng dẫn đến vấn đề truyền thông tin từ luồng này sang luồng khác. Các nguyên thủy giao tiếp tiêu chuẩn không giải quyết được vấn đề này. Do đó, chúng ta cần triển khai đối tượng tổng hợp của riêng mình để chia sẻ các đối tượng giữa các luồng để làm cho luồng giao tiếp an toàn. Sau đây là một số cấu trúc dữ liệu, cung cấp giao tiếp an toàn theo chuỗi sau khi thực hiện một số thay đổi trong chúng -
Bộ
Để sử dụng cấu trúc dữ liệu tập hợp theo cách an toàn luồng, chúng ta cần mở rộng lớp tập hợp để triển khai cơ chế khóa của riêng chúng ta.
Thí dụ
Đây là một ví dụ Python về việc mở rộng lớp -
class extend_class(set):
def __init__(self, *args, **kwargs):
self._lock = Lock()
super(extend_class, self).__init__(*args, **kwargs)
def add(self, elem):
self._lock.acquire()
try:
super(extend_class, self).add(elem)
finally:
self._lock.release()
def delete(self, elem):
self._lock.acquire()
try:
super(extend_class, self).delete(elem)
finally:
self._lock.release()
Trong ví dụ trên, một đối tượng lớp có tên extend_class đã được định nghĩa và được kế thừa từ Python set class. Một đối tượng khóa được tạo bên trong hàm tạo của lớp này. Bây giờ, có hai chức năng -add() và delete(). Các chức năng này được xác định và an toàn theo luồng. Cả hai đều dựa vàosuper chức năng lớp với một ngoại lệ chính.
Người trang trí
Đây là một phương pháp quan trọng khác để truyền thông an toàn theo luồng là sử dụng trình trang trí.
Thí dụ
Hãy xem xét một ví dụ Python cho thấy cách sử dụng trình trang trí & mminus;
def lock_decorator(method):
def new_deco_method(self, *args, **kwargs):
with self._lock:
return method(self, *args, **kwargs)
return new_deco_method
class Decorator_class(set):
def __init__(self, *args, **kwargs):
self._lock = Lock()
super(Decorator_class, self).__init__(*args, **kwargs)
@lock_decorator
def add(self, *args, **kwargs):
return super(Decorator_class, self).add(elem)
@lock_decorator
def delete(self, *args, **kwargs):
return super(Decorator_class, self).delete(elem)
Trong ví dụ trên, một phương thức decorator có tên lock_decorator đã được định nghĩa, phương thức này được kế thừa từ lớp phương thức Python. Sau đó, một đối tượng khóa được tạo bên trong hàm tạo của lớp này. Bây giờ, có hai hàm - thêm () và xóa (). Các chức năng này được xác định và an toàn theo luồng. Cả hai đều dựa trên chức năng siêu cấp với một ngoại lệ chính.
Danh sách
Cấu trúc dữ liệu danh sách an toàn theo luồng, nhanh chóng cũng như cấu trúc dễ dàng để lưu trữ tạm thời trong bộ nhớ. Trong Cpython, GIL bảo vệ chống lại việc truy cập đồng thời vào chúng. Như chúng ta đã biết rằng danh sách là an toàn theo chuỗi nhưng dữ liệu nằm trong đó thì sao. Trên thực tế, dữ liệu của danh sách không được bảo vệ. Ví dụ,L.append(x)không đảm bảo trả về kết quả mong đợi nếu một luồng khác đang cố gắng làm điều tương tự. Điều này là do, mặc dùappend() là một hoạt động nguyên tử và an toàn cho luồng nhưng luồng khác đang cố gắng sửa đổi dữ liệu của danh sách theo cách đồng thời, do đó chúng ta có thể thấy tác dụng phụ của điều kiện chủng tộc trên đầu ra.
Để giải quyết loại vấn đề này và sửa đổi dữ liệu một cách an toàn, chúng ta phải triển khai cơ chế khóa thích hợp, điều này đảm bảo hơn nữa rằng nhiều luồng không thể có khả năng chạy vào các điều kiện đua. Để thực hiện cơ chế khóa thích hợp, chúng ta có thể mở rộng lớp như chúng ta đã làm trong các ví dụ trước.
Một số hoạt động nguyên tử khác trên danh sách như sau:
L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()
Đây -
- L, L1, L2 tất cả đều là danh sách
- D, D1, D2 là các con số
- x, y là các đối tượng
- tôi, j là int
Hàng đợi
Nếu dữ liệu của danh sách không được bảo vệ, chúng tôi có thể phải đối mặt với hậu quả. Chúng tôi có thể lấy hoặc xóa mục dữ liệu sai, về điều kiện chủng tộc. Đó là lý do tại sao nên sử dụng cấu trúc dữ liệu hàng đợi. Một ví dụ thực tế về hàng đợi có thể là đường một chiều có một làn xe, nơi xe đi vào trước, ra trước. Có thể thấy nhiều ví dụ thực tế hơn về việc xếp hàng ở cửa sổ soát vé và trạm dừng xe buýt.
Theo mặc định, hàng đợi là cấu trúc dữ liệu an toàn theo luồng và chúng ta không cần lo lắng về việc triển khai cơ chế khóa phức tạp. Python cung cấp cho chúng ta
Các loại hàng đợi
Trong phần này, chúng ta sẽ tìm hiểu về các loại hàng đợi khác nhau. Python cung cấp ba tùy chọn hàng đợi để sử dụng từ<queue> mô-đun -
- Hàng đợi bình thường (FIFO, First in First out)
- LIFO, Lượt ra trước
- Priority
Chúng ta sẽ tìm hiểu về các hàng đợi khác nhau trong các phần tiếp theo.
Hàng đợi bình thường (FIFO, First in First out)
Nó là cách triển khai hàng đợi được sử dụng phổ biến nhất do Python cung cấp. Trong cơ chế xếp hàng này, ai đến trước sẽ nhận được dịch vụ trước. FIFO còn được gọi là hàng đợi bình thường. Hàng đợi FIFO có thể được biểu diễn như sau:
Triển khai Python của Hàng đợi FIFO
Trong python, hàng đợi FIFO có thể được thực hiện với luồng đơn cũng như đa luồng.
Hàng đợi FIFO với luồng đơn
Để triển khai hàng đợi FIFO với một luồng, Queuelớp sẽ triển khai một container cơ bản nhập trước, xuất trước. Các phần tử sẽ được thêm vào một "cuối" của chuỗi bằng cách sử dụngput()và bị xóa khỏi đầu kia bằng cách sử dụng get().
Thí dụ
Sau đây là một chương trình Python để thực hiện hàng đợi FIFO với một luồng:
import queue
q = queue.Queue()
for i in range(8):
q.put("item-" + str(i))
while not q.empty():
print (q.get(), end = " ")
Đầu ra
item-0 item-1 item-2 item-3 item-4 item-5 item-6 item-7
Kết quả đầu ra cho thấy rằng chương trình trên sử dụng một luồng duy nhất để minh họa rằng các phần tử được xóa khỏi hàng đợi theo thứ tự mà chúng được chèn vào.
Hàng đợi FIFO với nhiều chủ đề
Để triển khai FIFO với nhiều luồng, chúng ta cần xác định hàm myqueue (), được mở rộng từ mô-đun hàng đợi. Hoạt động của các phương thức get () và put () giống như đã thảo luận ở trên trong khi triển khai hàng đợi FIFO với một luồng đơn. Sau đó, để làm cho nó đa luồng, chúng ta cần khai báo và khởi tạo các luồng. Các luồng này sẽ sử dụng hàng đợi theo cách FIFO.
Thí dụ
Sau đây là một chương trình Python để triển khai hàng đợi FIFO với nhiều luồng
import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the queue".format(threading.current_thread(), item))
queue.task_done()
time.sleep(2)
q = queue.Queue()
for i in range(5):
q.put(i)
threads = []
for i in range(4):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Đầu ra
<Thread(Thread-3654, started 5044)> removed 0 from the queue
<Thread(Thread-3655, started 3144)> removed 1 from the queue
<Thread(Thread-3656, started 6996)> removed 2 from the queue
<Thread(Thread-3657, started 2672)> removed 3 from the queue
<Thread(Thread-3654, started 5044)> removed 4 from the queue
LIFO, Cuối cùng trong hàng đợi Xuất đầu tiên
Hàng đợi này sử dụng sự tương tự hoàn toàn trái ngược với hàng đợi FIFO (Đầu tiên xuất trước). Trong cơ chế xếp hàng này, ai đến sau cùng sẽ nhận được dịch vụ trước. Điều này tương tự để thực hiện cấu trúc dữ liệu ngăn xếp. Hàng đợi LIFO tỏ ra hữu ích trong khi triển khai tìm kiếm Theo độ sâu giống như các thuật toán của trí tuệ nhân tạo.
Triển khai Python của hàng đợi LIFO
Trong python, hàng đợi LIFO có thể được thực hiện với luồng đơn cũng như đa luồng.
Hàng đợi LIFO với một chuỗi đơn
Để triển khai hàng đợi LIFO với một chuỗi đơn, Queue lớp sẽ triển khai một vùng chứa cuối cùng vào, ra trước cơ bản bằng cách sử dụng cấu trúc Queue.LifoQueue. Bây giờ, đang gọiput(), các phần tử được thêm vào phần đầu của vùng chứa và được loại bỏ khỏi phần đầu cũng bằng cách sử dụng get().
Thí dụ
Sau đây là một chương trình Python để triển khai hàng đợi LIFO với một luồng:
import queue
q = queue.LifoQueue()
for i in range(8):
q.put("item-" + str(i))
while not q.empty():
print (q.get(), end=" ")
Output:
item-7 item-6 item-5 item-4 item-3 item-2 item-1 item-0
Kết quả đầu ra cho thấy rằng chương trình trên sử dụng một luồng duy nhất để minh họa rằng các phần tử được xóa khỏi hàng đợi theo thứ tự ngược lại mà chúng được chèn vào.
Hàng đợi LIFO với nhiều chủ đề
Việc triển khai tương tự như chúng ta đã thực hiện việc triển khai hàng đợi FIFO với nhiều luồng. Sự khác biệt duy nhất là chúng ta cần sử dụngQueue lớp sẽ triển khai một vùng chứa cuối cùng vào, ra trước cơ bản bằng cách sử dụng cấu trúc Queue.LifoQueue.
Thí dụ
Sau đây là một chương trình Python để triển khai hàng đợi LIFO với nhiều luồng:
import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the queue".format(threading.current_thread(), item))
queue.task_done()
time.sleep(2)
q = queue.LifoQueue()
for i in range(5):
q.put(i)
threads = []
for i in range(4):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Đầu ra
<Thread(Thread-3882, started 4928)> removed 4 from the queue
<Thread(Thread-3883, started 4364)> removed 3 from the queue
<Thread(Thread-3884, started 6908)> removed 2 from the queue
<Thread(Thread-3885, started 3584)> removed 1 from the queue
<Thread(Thread-3882, started 4928)> removed 0 from the queue
Hàng đợi ưu tiên
Trong hàng đợi FIFO và LIFO, thứ tự của các mục có liên quan đến thứ tự chèn. Tuy nhiên, có nhiều trường hợp mức độ ưu tiên quan trọng hơn thứ tự chèn. Chúng ta hãy xem xét một ví dụ trong thế giới thực. Giả sử an ninh tại sân bay đang kiểm tra những người thuộc các hạng mục khác nhau. Những người thuộc VVIP, nhân viên hàng không, nhân viên hải quan, các danh mục có thể được ưu tiên kiểm tra thay vì được kiểm tra trên cơ sở khi đến như đối với dân thường.
Một khía cạnh quan trọng khác cần được xem xét đối với hàng đợi ưu tiên là cách phát triển bộ lập lịch tác vụ. Một thiết kế phổ biến là phục vụ nhiệm vụ tác nhân nhất trên cơ sở ưu tiên trong hàng đợi. Cấu trúc dữ liệu này có thể được sử dụng để chọn các mục từ hàng đợi dựa trên giá trị ưu tiên của chúng.
Triển khai hàng đợi ưu tiên trong Python
Trong python, hàng đợi ưu tiên có thể được thực hiện với luồng đơn cũng như đa luồng.
Hàng đợi ưu tiên với một chuỗi đơn
Để triển khai hàng đợi ưu tiên với một luồng đơn, Queue lớp sẽ triển khai một tác vụ trên vùng chứa ưu tiên bằng cách sử dụng cấu trúc Queue.Hàng đợi ưu tiên. Bây giờ, đang gọiput(), các phần tử được thêm vào với một giá trị trong đó giá trị thấp nhất sẽ có mức độ ưu tiên cao nhất và do đó được truy xuất đầu tiên bằng cách sử dụng get().
Thí dụ
Hãy xem xét chương trình Python sau để triển khai hàng đợi Ưu tiên với luồng đơn:
import queue as Q
p_queue = Q.PriorityQueue()
p_queue.put((2, 'Urgent'))
p_queue.put((1, 'Most Urgent'))
p_queue.put((10, 'Nothing important'))
prio_queue.put((5, 'Important'))
while not p_queue.empty():
item = p_queue.get()
print('%s - %s' % item)
Đầu ra
1 – Most Urgent
2 - Urgent
5 - Important
10 – Nothing important
Trong kết quả đầu ra ở trên, chúng ta có thể thấy rằng hàng đợi đã lưu trữ các mục dựa trên mức độ ưu tiên - giá trị ít hơn có mức độ ưu tiên cao.
Hàng đợi ưu tiên với nhiều chủ đề
Việc triển khai tương tự như việc triển khai hàng đợi FIFO và LIFO với nhiều luồng. Sự khác biệt duy nhất là chúng ta cần sử dụngQueue lớp để khởi tạo mức độ ưu tiên bằng cách sử dụng cấu trúc Queue.PriorityQueue. Một sự khác biệt nữa là cách tạo hàng đợi. Trong ví dụ dưới đây, nó sẽ được tạo với hai tập dữ liệu giống nhau.
Thí dụ
Chương trình Python sau giúp thực hiện hàng đợi ưu tiên với nhiều luồng:
import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the queue".format(threading.current_thread(), item))
queue.task_done()
time.sleep(1)
q = queue.PriorityQueue()
for i in range(5):
q.put(i,1)
for i in range(5):
q.put(i,1)
threads = []
for i in range(2):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Đầu ra
<Thread(Thread-4939, started 2420)> removed 0 from the queue
<Thread(Thread-4940, started 3284)> removed 0 from the queue
<Thread(Thread-4939, started 2420)> removed 1 from the queue
<Thread(Thread-4940, started 3284)> removed 1 from the queue
<Thread(Thread-4939, started 2420)> removed 2 from the queue
<Thread(Thread-4940, started 3284)> removed 2 from the queue
<Thread(Thread-4939, started 2420)> removed 3 from the queue
<Thread(Thread-4940, started 3284)> removed 3 from the queue
<Thread(Thread-4939, started 2420)> removed 4 from the queue
<Thread(Thread-4940, started 3284)> removed 4 from the queue