เธรดการสื่อสาร

ในชีวิตจริงหากทีมงานคนหนึ่งกำลังทำงานร่วมกันควรมีการสื่อสารระหว่างกันเพื่อให้งานเสร็จสิ้นอย่างถูกต้อง การเปรียบเทียบแบบเดียวกันนี้ใช้ได้กับเธรดด้วย ในการเขียนโปรแกรมเพื่อลดเวลาในอุดมคติของโปรเซสเซอร์เราจึงสร้างเธรดหลายเธรดและกำหนดงานย่อยที่แตกต่างกันให้กับทุกเธรด ดังนั้นจึงต้องมีสิ่งอำนวยความสะดวกในการสื่อสารและพวกเขาควรโต้ตอบกันเพื่อให้งานเสร็จสิ้นในลักษณะที่ตรงกัน

พิจารณาประเด็นสำคัญต่อไปนี้ที่เกี่ยวข้องกับการสื่อสารระหว่างเธรด -

  • No performance gain - หากเราไม่สามารถสื่อสารอย่างเหมาะสมระหว่างเธรดและกระบวนการได้ประสิทธิภาพที่ได้รับจากการทำงานพร้อมกันและการขนานกันก็ไม่มีประโยชน์

  • Accomplish task properly - หากไม่มีกลไกการสื่อสารที่เหมาะสมระหว่างเธรดงานที่ได้รับมอบหมายจะไม่สามารถดำเนินการได้อย่างถูกต้อง

  • More efficient than inter-process communication - การสื่อสารระหว่างเธรดมีประสิทธิภาพและใช้งานง่ายกว่าการสื่อสารระหว่างกระบวนการเนื่องจากเธรดทั้งหมดภายในกระบวนการใช้พื้นที่แอดเดรสเดียวกันและไม่จำเป็นต้องใช้หน่วยความจำร่วม

โครงสร้างข้อมูล Python สำหรับการสื่อสารที่ปลอดภัยของเธรด

โค้ดมัลติเธรดเกิดปัญหาในการส่งผ่านข้อมูลจากเธรดหนึ่งไปยังเธรดอื่น พื้นฐานการสื่อสารมาตรฐานไม่สามารถแก้ปัญหานี้ได้ ดังนั้นเราจำเป็นต้องใช้วัตถุผสมของเราเองเพื่อแบ่งปันวัตถุระหว่างเธรดเพื่อให้เธรดการสื่อสารปลอดภัย ต่อไปนี้เป็นโครงสร้างข้อมูลบางส่วนซึ่งให้การสื่อสารที่ปลอดภัยต่อเธรดหลังจากทำการเปลี่ยนแปลงบางอย่าง -

ชุด

สำหรับการใช้โครงสร้างข้อมูลชุดในลักษณะที่ปลอดภัยของเธรดเราจำเป็นต้องขยายคลาสชุดเพื่อใช้กลไกการล็อกของเราเอง

ตัวอย่าง

นี่คือตัวอย่าง Python ของการขยายคลาส -

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()

ในตัวอย่างข้างต้นคลาสอ็อบเจ็กต์ชื่อ extend_class ได้รับการกำหนดซึ่งสืบทอดต่อมาจาก Python set class. ล็อกอ็อบเจ็กต์ถูกสร้างขึ้นภายในคอนสตรัคเตอร์ของคลาสนี้ ตอนนี้มีสองฟังก์ชั่น -add() และ delete(). ฟังก์ชันเหล่านี้ถูกกำหนดและปลอดภัยต่อเธรด ทั้งคู่พึ่งพาไฟล์super ฟังก์ชันคลาสที่มีข้อยกเว้นคีย์เดียว

มัณฑนากร

นี่เป็นอีกวิธีการสำคัญสำหรับการสื่อสารที่ปลอดภัยต่อเธรดคือการใช้มัณฑนากร

ตัวอย่าง

ลองพิจารณาตัวอย่าง Python ที่แสดงวิธีใช้มัณฑนากร & 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)

ในตัวอย่างข้างต้นมีการกำหนดเมธอดมัณฑนากรชื่อ lock_decorator ซึ่งสืบทอดเพิ่มเติมจากคลาสเมธอด Python จากนั้นล็อกอ็อบเจ็กต์จะถูกสร้างขึ้นภายในคอนสตรัคเตอร์ของคลาสนี้ ตอนนี้มีสองฟังก์ชั่น - เพิ่ม () และลบ () ฟังก์ชันเหล่านี้ถูกกำหนดและปลอดภัยต่อเธรด ทั้งคู่พึ่งพาฟังก์ชันระดับซูเปอร์คลาสโดยมีข้อยกเว้นหนึ่งคีย์

รายการ

โครงสร้างข้อมูลรายการเป็นโครงสร้างที่ปลอดภัยรวดเร็วและง่ายสำหรับการจัดเก็บชั่วคราวในหน่วยความจำ ใน Cpython GIL ป้องกันการเข้าถึงพร้อมกัน อย่างที่เราทราบกันดีว่ารายการนั้นปลอดภัยสำหรับเธรด แต่ข้อมูลที่อยู่ในนั้นจะเป็นอย่างไร จริงๆแล้วข้อมูลของรายการไม่ได้รับการปกป้อง ตัวอย่างเช่น,L.append(x)ไม่รับประกันว่าจะส่งคืนผลลัพธ์ที่คาดหวังหากเธรดอื่นพยายามทำสิ่งเดียวกัน เนื่องจากแม้ว่าappend() เป็นการดำเนินการแบบอะตอมและเธรดที่ปลอดภัย แต่เธรดอื่นกำลังพยายามแก้ไขข้อมูลของรายการในแบบพร้อมกันดังนั้นเราจึงสามารถเห็นผลข้างเคียงของสภาวะการแข่งขันที่มีต่อผลลัพธ์

ในการแก้ไขปัญหาประเภทนี้และแก้ไขข้อมูลอย่างปลอดภัยเราต้องใช้กลไกการล็อกที่เหมาะสมซึ่งจะช่วยให้มั่นใจได้ว่าเธรดจำนวนมากไม่สามารถทำงานในสภาวะการแข่งขันได้ ในการใช้กลไกการล็อคที่เหมาะสมเราสามารถขยายคลาสได้เหมือนที่เคยทำในตัวอย่างก่อนหน้านี้

ปฏิบัติการปรมาณูอื่น ๆ ในรายการมีดังนี้ -

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()

ที่นี่ -

  • L, L1, L2 ทั้งหมดเป็นรายการ
  • D, D1, D2 เป็นคำสั่ง
  • x, y คือวัตถุ
  • ผม j เป็น ints

คิว

หากข้อมูลของรายการไม่ได้รับการปกป้องเราอาจต้องเผชิญกับผลที่ตามมา เราอาจได้รับหรือลบรายการข้อมูลผิดเงื่อนไขการแข่งขัน นั่นคือเหตุผลที่แนะนำให้ใช้โครงสร้างข้อมูลคิว ตัวอย่างคิวในโลกแห่งความเป็นจริงอาจเป็นถนนทางเดียวเลนเดียวซึ่งรถเข้าก่อนออกก่อน ตัวอย่างอื่น ๆ ในโลกแห่งความเป็นจริงสามารถดูได้ที่หน้าต่างตั๋วและป้ายรถเมล์

คิวเป็นโครงสร้างข้อมูลที่ปลอดภัยโดยค่าเริ่มต้นและเราไม่จำเป็นต้องกังวลเกี่ยวกับการใช้กลไกการล็อกที่ซับซ้อน Python ให้ไฟล์ โมดูลเพื่อใช้คิวประเภทต่างๆในแอปพลิเคชันของเรา

ประเภทของคิว

ในส่วนนี้เราจะได้รับเกี่ยวกับคิวประเภทต่างๆ Python มีคิวสามตัวเลือกให้ใช้จากไฟล์<queue> โมดูล -

  • คิวปกติ (FIFO ก่อนออกก่อน)
  • LIFO สุดท้ายก่อนออก
  • Priority

เราจะเรียนรู้เกี่ยวกับคิวต่างๆในส่วนต่อไป

คิวปกติ (FIFO ก่อนออกก่อน)

เป็นที่นิยมใช้กันมากที่สุดการใช้งานคิวที่เสนอโดย Python ในกลไกการเข้าคิวนี้ใครจะมาก่อนจะได้รับบริการก่อน FIFO เรียกอีกอย่างว่าคิวปกติ คิว FIFO สามารถแสดงได้ดังนี้ -

การใช้งาน Python ของคิว FIFO

ใน python คิว FIFO สามารถใช้งานได้กับเธรดเดี่ยวและมัลติเธรด

คิว FIFO พร้อมเธรดเดียว

สำหรับการนำคิว FIFO ไปใช้กับเธรดเดียวไฟล์ Queueคลาสจะใช้คอนเทนเนอร์แบบเข้าก่อนออกก่อนขั้นพื้นฐาน องค์ประกอบจะถูกเพิ่มลงใน "ส่วนท้าย" ของลำดับโดยใช้put()และนำออกจากปลายอีกด้านหนึ่งโดยใช้ get().

ตัวอย่าง

ต่อไปนี้เป็นโปรแกรม Python สำหรับใช้คิว FIFO ด้วยเธรดเดียว -

import queue

q = queue.Queue()

for i in range(8):
   q.put("item-" + str(i))

while not q.empty():
   print (q.get(), end = " ")

เอาต์พุต

item-0 item-1 item-2 item-3 item-4 item-5 item-6 item-7

ผลลัพธ์แสดงให้เห็นว่าโปรแกรมข้างต้นใช้เธรดเดียวเพื่อแสดงให้เห็นว่าองค์ประกอบถูกลบออกจากคิวในลำดับเดียวกับที่แทรก

คิว FIFO ที่มีหลายเธรด

สำหรับการนำ FIFO ไปใช้กับหลายเธรดเราจำเป็นต้องกำหนดฟังก์ชัน myqueue () ซึ่งขยายจากโมดูลคิว การทำงานของเมธอด get () และ put () เหมือนกับที่กล่าวไว้ข้างต้นในขณะที่ใช้คิว FIFO กับเธรดเดี่ยว จากนั้นในการทำให้เป็นมัลติเธรดเราจำเป็นต้องประกาศและสร้างอินสแตนซ์ของเธรด เธรดเหล่านี้จะใช้คิวในลักษณะ FIFO

ตัวอย่าง

ต่อไปนี้เป็นโปรแกรม Python สำหรับการใช้งานคิว FIFO ที่มีหลายเธรด

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()

เอาต์พุต

<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 สุดท้ายในคิวก่อนออก

คิวนี้ใช้การเปรียบเทียบที่ตรงกันข้ามกับคิว FIFO (First in First Out) ในกลไกการจัดคิวนี้ผู้ที่มาสุดท้ายจะได้รับบริการก่อน ซึ่งคล้ายกับการใช้โครงสร้างข้อมูลสแต็ก คิว LIFO พิสูจน์ได้ว่ามีประโยชน์ในขณะที่ใช้การค้นหาเชิงลึกเป็นอันดับแรกเช่นอัลกอริทึมของปัญญาประดิษฐ์

การใช้งาน Python ของคิว LIFO

ใน python สามารถใช้คิว LIFO กับเธรดเดี่ยวและมัลติเธรดได้

LIFO คิวด้วยเธรดเดียว

สำหรับการใช้งาน LIFO คิวด้วยเธรดเดียวไฟล์ Queue คลาสจะใช้งานคอนเทนเนอร์ที่เข้าก่อนออกก่อนขั้นพื้นฐานโดยใช้โครงสร้าง Queue.LifoQueue. ตอนนี้กำลังโทรput()องค์ประกอบจะถูกเพิ่มเข้าไปในส่วนหัวของภาชนะและนำออกจากหัวด้วยเมื่อใช้ get().

ตัวอย่าง

ต่อไปนี้เป็นโปรแกรม Python สำหรับการใช้งานคิว LIFO ด้วยเธรดเดียว -

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

ผลลัพธ์แสดงให้เห็นว่าโปรแกรมด้านบนใช้เธรดเดียวเพื่อแสดงให้เห็นว่าอิลิเมนต์ถูกลบออกจากคิวในลำดับที่ตรงกันข้ามกับที่ถูกแทรก

LIFO คิวที่มีหลายเธรด

การนำไปใช้นั้นคล้ายกับที่เราได้ดำเนินการตามคิว FIFO ที่มีหลายเธรด ข้อแตกต่างเพียงอย่างเดียวคือเราต้องใช้ไฟล์Queue คลาสที่จะใช้งานคอนเทนเนอร์แบบ last-in, first-out ขั้นพื้นฐานโดยใช้โครงสร้าง Queue.LifoQueue.

ตัวอย่าง

ต่อไปนี้เป็นโปรแกรม Python สำหรับการใช้งาน LIFO Queue ที่มีหลายเธรด -

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()

เอาต์พุต

<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

ลำดับความสำคัญ

ในคิว FIFO และ LIFO ลำดับของรายการจะเกี่ยวข้องกับลำดับการแทรก อย่างไรก็ตามมีหลายกรณีที่ลำดับความสำคัญสำคัญกว่าลำดับของการแทรก ขอให้เราพิจารณาตัวอย่างของโลกแห่งความเป็นจริง สมมติว่าเจ้าหน้าที่รักษาความปลอดภัยที่สนามบินกำลังตรวจสอบบุคคลประเภทต่างๆ คนของ VVIP, เจ้าหน้าที่สายการบิน, เจ้าหน้าที่กำหนดเอง, หมวดหมู่อาจได้รับการตรวจสอบตามลำดับความสำคัญแทนที่จะถูกตรวจสอบบนพื้นฐานของการมาถึงเช่นเดียวกับสามัญชน

สิ่งสำคัญอีกประการหนึ่งที่ต้องพิจารณาสำหรับลำดับความสำคัญคือวิธีการพัฒนาตัวกำหนดตารางเวลางาน การออกแบบทั่วไปอย่างหนึ่งคือการให้บริการงานตัวแทนส่วนใหญ่ตามลำดับความสำคัญในคิว โครงสร้างข้อมูลนี้สามารถใช้เพื่อรับรายการจากคิวตามค่าลำดับความสำคัญ

การใช้งาน Python ของคิวลำดับความสำคัญ

ใน python ลำดับความสำคัญสามารถใช้กับเธรดเดี่ยวและมัลติเธรดได้

ลำดับความสำคัญของคิวด้วยเธรดเดียว

สำหรับการนำลำดับความสำคัญไปใช้กับเธรดเดี่ยวไฟล์ Queue คลาสจะใช้งานบนคอนเทนเนอร์ลำดับความสำคัญโดยใช้โครงสร้าง Queue.PriorityQueue. ตอนนี้กำลังโทรput()องค์ประกอบจะถูกเพิ่มด้วยค่าโดยที่ค่าต่ำสุดจะมีลำดับความสำคัญสูงสุดและด้วยเหตุนี้จึงถูกดึงมาก่อนโดยใช้ get().

ตัวอย่าง

พิจารณาโปรแกรม Python ต่อไปนี้สำหรับการใช้งาน Priority Queue ด้วย single thread -

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)

เอาต์พุต

1 – Most Urgent
2 - Urgent
5 - Important
10 – Nothing important

ในผลลัพธ์ข้างต้นเราจะเห็นว่าคิวได้จัดเก็บรายการตามลำดับความสำคัญ - ค่าที่น้อยกว่ามีลำดับความสำคัญสูง

ลำดับความสำคัญของคิวที่มีหลายเธรด

การนำไปใช้งานจะคล้ายกับการใช้คิว FIFO และ LIFO ที่มีหลายเธรด ข้อแตกต่างเพียงอย่างเดียวคือเราต้องใช้ไฟล์Queue คลาสสำหรับการเริ่มต้นลำดับความสำคัญโดยใช้โครงสร้าง Queue.PriorityQueue. ความแตกต่างอีกประการหนึ่งคือวิธีการสร้างคิว ในตัวอย่างด้านล่างนี้จะสร้างขึ้นด้วยชุดข้อมูลที่เหมือนกันสองชุด

ตัวอย่าง

โปรแกรม Python ต่อไปนี้ช่วยในการดำเนินการตามลำดับความสำคัญกับหลายเธรด -

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()

เอาต์พุต

<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