การซิงโครไนซ์เธรด

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

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

ปัญหาในการซิงโครไนซ์เธรด

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

  • Deadlock
  • สภาพการแข่งขัน

สภาพการแข่งขัน

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

ตัวอย่าง

ลองพิจารณาตัวอย่างนี้เพื่อทำความเข้าใจแนวคิดเรื่องสภาพการแข่งขัน -

Step 1 - ในขั้นตอนนี้เราต้องนำเข้าโมดูลเธรด -

import threading

Step 2 - ตอนนี้กำหนดตัวแปรส่วนกลางพูด x พร้อมกับค่าเป็น 0 -

x = 0

Step 3 - ตอนนี้เราต้องกำหนดไฟล์ increment_global() ซึ่งจะเพิ่มขึ้นทีละ 1 ในฟังก์ชันโกลบอลนี้ x -

def increment_global():

   global x
   x += 1

Step 4 - ในขั้นตอนนี้เราจะกำหนดไฟล์ taskofThread()ฟังก์ชันซึ่งจะเรียกใช้ฟังก์ชัน Increment_global () ตามจำนวนครั้งที่ระบุ สำหรับตัวอย่างของเราคือ 50000 ครั้ง -

def taskofThread():

   for _ in range(50000):
      increment_global()

Step 5- ตอนนี้กำหนดฟังก์ชัน main () ที่สร้างเธรด t1 และ t2 ทั้งสองอย่างจะเริ่มต้นด้วยความช่วยเหลือของฟังก์ชัน start () และรอจนกว่าพวกเขาจะทำงานเสร็จด้วยความช่วยเหลือของฟังก์ชัน join ()

def main():
   global x
   x = 0
   
   t1 = threading.Thread(target= taskofThread)
   t2 = threading.Thread(target= taskofThread)

   t1.start()
   t2.start()

   t1.join()
   t2.join()

Step 6- ตอนนี้เราต้องระบุช่วงสำหรับจำนวนการวนซ้ำที่เราต้องการเรียกใช้ฟังก์ชัน main () นี่เราเรียกมันมา 5 ครั้งแล้ว

if __name__ == "__main__":
   for i in range(5):
      main()
      print("x = {1} after Iteration {0}".format(i,x))

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

เอาต์พุต

x = 100000 after Iteration 0
x = 54034 after Iteration 1
x = 80230 after Iteration 2
x = 93602 after Iteration 3
x = 93289 after Iteration 4

การจัดการกับสภาพการแข่งขันโดยใช้การล็อก

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

ได้รับ () วิธีการ

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

  • With value set to True - ถ้าเมธอด acquire () ถูกเรียกใช้ด้วย True ซึ่งเป็นอาร์กิวเมนต์เริ่มต้นการทำงานของเธรดจะถูกบล็อกจนกว่าการล็อกจะถูกปลดล็อก

  • With value set to False - ถ้าเมธอด acquire () ถูกเรียกใช้ด้วย False ซึ่งไม่ใช่อาร์กิวเมนต์เริ่มต้นการดำเนินการเธรดจะไม่ถูกบล็อกจนกว่าจะถูกตั้งค่าเป็นจริงกล่าวคือจนกว่าจะถูกล็อก

release () วิธีการ

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

  • หากล็อคถูกล็อคแสดงว่าไฟล์ release()วิธีการจะปลดล็อก หน้าที่ของมันคืออนุญาตให้เธรดหนึ่งเธรดดำเนินการต่อหากมีมากกว่าหนึ่งเธรดถูกบล็อกและรอให้ล็อกปลดล็อก

  • มันจะเพิ่ม ThreadError หากล็อคถูกปลดล็อคแล้ว

ตอนนี้เราสามารถเขียนโปรแกรมข้างต้นใหม่ด้วยคลาสล็อคและวิธีการเพื่อหลีกเลี่ยงสภาวะการแข่งขัน เราจำเป็นต้องกำหนดเมธอด taskofThread () ด้วยอาร์กิวเมนต์ lock จากนั้นจำเป็นต้องใช้เมธอด acquire () และ release () ในการบล็อกและไม่บล็อกการล็อกเพื่อหลีกเลี่ยงสภาวะการแข่งขัน

ตัวอย่าง

ต่อไปนี้เป็นตัวอย่างของโปรแกรม python เพื่อทำความเข้าใจแนวคิดของการล็อคเพื่อจัดการกับสภาพการแข่งขัน -

import threading

x = 0

def increment_global():

   global x
   x += 1

def taskofThread(lock):

   for _ in range(50000):
      lock.acquire()
      increment_global()
      lock.release()

def main():
   global x
   x = 0

   lock = threading.Lock()
   t1 = threading.Thread(target = taskofThread, args = (lock,))
   t2 = threading.Thread(target = taskofThread, args = (lock,))

   t1.start()
   t2.start()

   t1.join()
   t2.join()

if __name__ == "__main__":
   for i in range(5):
      main()
      print("x = {1} after Iteration {0}".format(i,x))

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

เอาต์พุต

x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4

Deadlocks - ปัญหานักปรัชญาการรับประทานอาหาร

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

Edsger Dijkstra เดิมนำเสนอปัญหาปราชญ์ด้านการรับประทานอาหารซึ่งเป็นหนึ่งในภาพประกอบที่มีชื่อเสียงของปัญหาใหญ่ที่สุดอย่างหนึ่งของระบบที่เกิดขึ้นพร้อมกันที่เรียกว่าการหยุดชะงัก

ในปัญหานี้มีนักปรัชญาที่มีชื่อเสียงห้าคนนั่งอยู่ที่โต๊ะกลมกินอาหารจากชามของพวกเขา มีส้อมห้าอันที่นักปรัชญาทั้งห้าคนสามารถใช้กินอาหารได้ อย่างไรก็ตามนักปรัชญาตัดสินใจที่จะใช้ส้อมสองอันในเวลาเดียวกันเพื่อกินอาหารของพวกเขา

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

การหยุดชะงักในระบบพร้อมกัน

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

วิธีแก้ปัญหาด้วยโปรแกรม Python

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

ตัวอย่าง

โปรแกรม Python ต่อไปนี้จะช่วยเราหาวิธีแก้ปัญหานักปรัชญาการรับประทานอาหาร -

import threading
import random
import time

class DiningPhilosopher(threading.Thread):

   running = True

   def __init__(self, xname, Leftfork, Rightfork):
   threading.Thread.__init__(self)
   self.name = xname
   self.Leftfork = Leftfork
   self.Rightfork = Rightfork

   def run(self):
   while(self.running):
      time.sleep( random.uniform(3,13))
      print ('%s is hungry.' % self.name)
      self.dine()

   def dine(self):
   fork1, fork2 = self.Leftfork, self.Rightfork

   while self.running:
      fork1.acquire(True)
      locked = fork2.acquire(False)
	  if locked: break
      fork1.release()
      print ('%s swaps forks' % self.name)
      fork1, fork2 = fork2, fork1
   else:
      return

   self.dining()
   fork2.release()
   fork1.release()

   def dining(self):
   print ('%s starts eating '% self.name)
   time.sleep(random.uniform(1,10))
   print ('%s finishes eating and now thinking.' % self.name)

def Dining_Philosophers():
   forks = [threading.Lock() for n in range(5)]
   philosopherNames = ('1st','2nd','3rd','4th', '5th')

   philosophers= [DiningPhilosopher(philosopherNames[i], forks[i%5], forks[(i+1)%5]) \
      for i in range(5)]

   random.seed()
   DiningPhilosopher.running = True
   for p in philosophers: p.start()
   time.sleep(30)
   DiningPhilosopher.running = False
   print (" It is finishing.")

Dining_Philosophers()

โปรแกรมข้างต้นใช้แนวคิดของนักปรัชญาที่ละโมบและใจกว้าง โปรแกรมนี้ยังใช้ไฟล์acquire() และ release() วิธีการของ Lock คลาสของ <threading>โมดูล. เราสามารถดูวิธีแก้ปัญหาได้ในผลลัพธ์ต่อไปนี้ -

เอาต์พุต

4th is hungry.
4th starts eating
1st is hungry.
1st starts eating
2nd is hungry.
5th is hungry.
3rd is hungry.
1st finishes eating and now thinking.3rd swaps forks
2nd starts eating
4th finishes eating and now thinking.
3rd swaps forks5th starts eating
5th finishes eating and now thinking.
4th is hungry.
4th starts eating
2nd finishes eating and now thinking.
3rd swaps forks
1st is hungry.
1st starts eating
4th finishes eating and now thinking.
3rd starts eating
5th is hungry.
5th swaps forks
1st finishes eating and now thinking.
5th starts eating
2nd is hungry.
2nd swaps forks
4th is hungry.
5th finishes eating and now thinking.
3rd finishes eating and now thinking.
2nd starts eating 4th starts eating
It is finishing.