Thread ในภาษา Python
ในบทนี้ คุณจะได้เรียนรู้เกี่ยวกับ Thread ในภาษา Python เราจะพูดเกี่ยวกับการสร้างและใช้งาน Thread และวิธีการนำ Thread มาช่วยเพื่อเพิ่มประสิทธิภาพให้กับโปรแกรมของเรา
Thread คืออะไร
Thread (เทร็ด) คือลำดับการทำงานของชุดคำสั่งโปรแกรมที่เล็กที่สุดซึ่งโดยทั่วไปแล้วจะอยู่ภายใน Process โดยที่ในหนึ่ง Process นั้นจะสามารถมีได้หลาย Thread การใช้งาน Thread จะทำให้เราสามารถเขียนโปรแกรมที่ทำงานแบบคู่ขนานและใช้ทรัพยากรบางอย่างร่วมกัน ยกตัวอย่างเช่น หน่วยความจำ เมื่อมีหลาย Thread ทำงานพร้อมกัน เราจะเรียกการเขียนโปรแกรมในรูปแบบนี้ว่า Multi-thread
รูปภาพแสดงการทำงานของ Thread และ Process
โดยทั่วไปแล้ว โปรแกรมที่เราเขียนในภาษา Python นั้นจะรันอยู่ภายใน Thread หลัก ซึ่งเป็น Thread เริ่มต้นเมื่อโปรแกรมของภาษา Python เริ่มทำงาน ซึ่งภายใน Thread หลักนี้เอง เราสามารถสร้าง Thread อื่นๆ เพื่อใช้ประโยชน์จากมันได้
การสร้าง Thread ในภาษา Python
ในการสร้าง Thread นั้นเราสามารถสร้างได้จากคลาส Thread
ที่อยู่ภายในโมดูล threading
ซึ่งโมดูลนี้ประกอบไปด้วยคลาสต่างๆ ที่ใช้สำหรับสร้างและทำงานเกี่ยวกับ Thread นี่เป็นตัวอย่างการสร้าง Thread ในภาษา Python
import threading
def thread_callback():
print("Hello inside Thread")
thr = threading.Thread(target=thread_callback)
thr.start()
นี่เป็นผลลัพธ์การทำงานของโปรแกรม
Hello inside Thread
ในตัวอย่าง เราได้สร้าง Thread อย่างง่ายขึ้นมา โดย Thread นี้ใช้สำหรับแสดงข้อความทักทายง่ายๆ จากภายใน Thread
import threading
คลาส Thread
เป็นคลาสที่อยู่ภายใต้โมดูล theading
ดังนั้นเราจึงต้องทำการ import โมดูลเข้ามาก่อนที่จะใช้งานคลาสที่ต้องการได้
thr = threading.Thread(target=thread_callback)
thr.start()
หลังจากนั้นเราสร้าง Thread ใหม่ขึ้นมาเก็บไว้ในตัวแปร thr
ในคอนสตรัคเตอร์นั้นเราสามารถส่งอาร์กิวเมนต์ target
ซึ่งจะต้องเป็นฟังก์ชันที่สามารถเรียกใช้งานได้ และฟังก์ชันนี้จะทำงานเมื่อเราเรียกเมธอด start()
บนออบเจ็ค Thread เพื่อให้ Thread เริ่มทำงาน
ในบางครั้ง คุณอาจจะต้องการส่งค่าเพื่อนำเข้าไปใช้งานในฟังก์ชัน callback คุณสามารถทำได้โดยการส่งผ่านอาร์กิวเมนต์ args
ดังในตัวอย่างต่อไปนี้
import threading
def thread_callback(name, loop):
for i in range(1, loop + 1):
print("%s: %i" % (name, i))
thr = threading.Thread(target=thread_callback, args=["Thread-1", 5])
thr.start()
ในตัวอย่าง เราได้กำหนดค่าให้กับฟังก์ชัน thread_callback
โดยการกำหนดผ่านอาร์กิวเมนต์ args
ซึ่งเป็นอาเรย์ของค่าที่เราต้องการส่งเข้าไป เมื่อเราเรียกใช้งานเมธอด start()
ค่าที่เราส่งผ่าน args
จะถูกส่งเข้าไปยังพารามิเตอร์ name
และ loop
ตามลำดับ
โดยที่ name
นั้นเป็นชื่อที่เราต้องการกำหนดให้กับ Thread ส่วน loop
นั้นเป็นจำนวนรอบที่เราต้องการแสดงตัวเลขภายใน Thread
Thread-1: 1
Thread-1: 2
Thread-1: 3
Thread-1: 4
Thread-1: 5
นี่เป็นผลลัพธ์การทำงานของโปรแกรม
การสร้างคลาส Thread
ในตัวอย่างก่อนหน้า เราได้สร้าง Thread ออบเจ็คจากคลาส Thread ซึ่งเป็นคลาสไลบรารี่ของภาษา Python ที่ใช้สำหรับสร้าง Thread ที่ไม่ซับซ้อนมาก อย่างไรก็ตาม คุณสามารถสร้างคลาส Thread ของคุณได้โดยการสืบทอดคลาสของคุณจากคลาส Thread
นี่เป็นตัวอย่าง
from threading import Thread
class MyThread(Thread):
def __init__(self, firstName):
Thread.__init__(self)
self.firstName = firstName
def run(self):
print("Hello " + self.firstName + " from " + self.name)
thr1 = MyThread("Meteo")
thr2 = MyThread("Danny")
thr1.start()
thr2.start()
นี่เป็นผลลัพธ์การทำงานของโปรแกรม
Hello Meteo from Thread-1
Hello Danny from Thread-2
ในตัวอย่าง เราได้สร้างคลาสที่มีชื่อว่า MyThread
และคลาสนี้ได้ทำการสืบทอดคลาสมาจากคลาส Thread
ภายในคอนสตรัคเตอร์ของคลาสมีหนึ่งอาร์กิวเมนต์ firstName
สำหรับกำหนดชื่อที่ต้องการแสดงการทักทายใน Thread
def run(self):
print("Hello " + self.firstName + " from " + self.name)
คลาสที่ทำการสืบทอดมาจากคลาส Thread
นั้นจะต้องทำการ override เมธอด run()
ซึ่งเมธอดนี้จะทำงานเมื่อเราเรียกใช้งานเมธอด start()
บนออบเจ็คของ Thread โดยเมธอดนี้ทำหน้าเหมือนกับเมธอด thread_callback()
ในตัวอย่างก่อนหน้า
ภายในเมธอด run()
มีสองแอตทริบิวต์ที่เราแสดงออกมาทางหน้าจอ แอตทริบิวต์แรกคือ self.firstName
ซึ่งเป็นชื่อของคนที่ต้องการกล่าวทักทายที่เราส่งมาในตอนสร้างออบเจ็ค และแอตทริบิวต์ที่สอง self.name
นั้นเป็นแอตทริบิวต์ชื่อของ Thread ที่สืบทอดมาจากคลาส Thread
thr1 = MyThread("Meteo")
thr2 = MyThread("Danny")
thr1.start()
thr2.start()
หลังจากนั้นเราได้นำคลาสที่เราสร้างขึ้นมาสร้างออบเจ็ค Thread โดยการส่งชื่อที่ต้องการให้โปรแกรมกล่าวคำทักทายผ่านทางคอนสตรัคเตอร์ของคลาส และเรียกใช้งานเมธอด start()
เพื่อให้ Thread เริ่มทำงาน
การเขียนโปรแกรมแบบ Multi-thread
Thread นั้นจะทำงานพร้อมกันแบบคู่ขนาน รวมทั้ง Thread หลักด้วย เพื่อแสดงให้คุณได้เห็น มาดูตัวอย่างของ Thread ที่มีการทำงานนานกว่าในตัวอย่างที่เราได้เห็นก่อนหน้า เราจะเขียนโปรแกรมเพื่อแสดงตัวเลข 1 - 5
จากภายใน Thread
from threading import Thread
class Counter(Thread):
def __init__(self, end):
Thread.__init__(self)
self.end = end
def run(self):
for i in range(1, self.end + 1):
print(self.name + ": " + str(i))
thr1 = Counter(5)
thr2 = Counter(5)
thr1.start()
thr2.start()
จากในโค้ดตัวอย่างนั้น คลาส Counter
นั้นเป็นคลาสที่สืบทอดมาจากคลาส Thread
หน้าที่ของมันคือการแสดงตัวเลขระหว่าง 1 - end
ภายใน Thread โดยที่ค่า end
เป็นค่าที่ส่งเข้ามาในตอนสร้าง Thread
def run(self):
for i in range(1, self.end + 1):
print(self.name + ": " + str(i))
ภายในเมธอด run()
เราได้เขียนคำสั่ง for loop เพื่อวนแสดงตัวเลขจาก 1 - end
โดยที่ในตอนสร้าง Thread ทั้งสองเราได้ส่งค่า end
ซึ่งมีค่าเท่ากับ 5
ดังนั้น Thread จะแสดงตัวเลขจาก 1 - 5 นั่นเอง และนอกจากนี้เรายังได้แสดงชื่อของ Thread จากแอตทริบิวต์ self.name
เพื่อบอกให้ทราบว่าตัวเลขทีแสดงนั้นทำงานอยู่ใน Thread ไหน
thr1 = Counter(5)
thr2 = Counter(5)
thr1.start()
thr2.start()
หลังจากที่เราสร้างคลาสเสร็จสิ้น เราได้นำคลาสของเรามาสร้าง Thread ออบเจ็ค โดยการกำหนดค่าสิ้นสุดของการนับเป็น 5
ผ่านทางคอนสตรัคเตอร์ คุณสามารถเปลี่ยนเป็นค่าอื่นได้ เพื่อให้โปรแกรมนับไปถึงจำนวนที่ต้องการ
นี่เป็นผลลัพธ์การทำงานของโปรแกรม
Thread-1: 1
Thread-2: 1
Thread-1: 2
Thread-2: 2
Thread-1: 3
Thread-2: 3
Thread-1: 4
Thread-1: 5
Thread-2: 4
Thread-2: 5
จากตัวอย่างด้านบน เราสามารถเขียนในรูปแบบของฟังก์ชัน callback ได้ดังนี้ ซึ่งทั้งสองวิธีนั้นให้ผลลัพธ์ที่เหมือนกัน แต่ในวิธีนี้ คุณจะไม่สามารถเข้าถึงแอตทริบิวต์ self.name
สำหรับชื่อของ Thread ที่สร้างอัตโนมัติได้ ดังนั้นถ้าหากต้องการกำหนดชื่อให้กับ Thread เราจำเป็นต้องกำหนดผ่านทางอาร์กิวเมนต์ args
เพื่อส่งเข้าไปยังฟังก์ชันแทน
from threading import Thread
def thread_callback(name, end):
for i in range(1, end + 1):
print(name + ": " + str(i))
thr1 = Thread(target=thread_callback, args=['Thread-1', 5])
thr2 = Thread(target=thread_callback, args=['Thread-2', 5])
thr1.start()
thr2.start()
Thread Joining
เมธอด join()
นั้นเป็นเมธอดที่รอให้ Thread ทำงานให้จบก่อนที่จะทำในคำสั่งต่อไป เราใช้เมธอดนี้ในกรณีที่ต้องการรอการทำงานบางอย่างจาก Thread ยกตัวอย่างเช่น ก่อนที่จะทำงาน Thread ที่สองอาจจะต้องรอผลลัพธ์การทำงานจาก Thread แรกก่อน ดังนั้นก่อนที่เรียกใช้งาน Thread ที่สอง เราจำเป็นต้องรอให้ Thread แรกทำงานเสร็จก่อน มาดูตัวอย่าง
from threading import Thread
class Counter(Thread):
def __init__(self, end):
Thread.__init__(self)
self.end = end
def run(self):
for i in range(1, self.end + 1):
print(self.name + ": " + str(i))
thr1 = Counter(5)
thr1.start()
# Block until thread 1 is done
thr1.join()
thr2 = Counter(5)
thr2.start()
ในตัวอย่าง เป็นโปรแกรมนับตัวเลขเหมือนในตัวอย่างก่อนหน้า แต่ในตัวอย่างนี้ เราต้องการให้โปรแกรมทำงานใน Thread แรกเสร็จก่อน แล้วค่อยทำ Thread ที่สองต่อ ดังนั้นก่อนที่จะเริ่มการทำงานของ Thread ที่สอง เราสามารถเรียกใช้งานเมธอด join()
เพื่อให้โปรแกรมรอการทำงานใน Thread แรกให้เสร็จก่อนได้
Thread-1: 1
Thread-1: 2
Thread-1: 3
Thread-1: 4
Thread-1: 5
Thread-2: 1
Thread-2: 2
Thread-2: 3
Thread-2: 4
Thread-2: 5
นี่เป็นผลลัพธ์การทำงานของโปรแกรม สังเกตว่าโปรแกรมจะแสดงตัวเลขใน Thread แรกจนเสร็จก่อนแล้วค่อยทำงานใน Thread ที่สอง นั่นเป็นเพราะผลของคำสั่ง join() ที่เราเรียกใช้งานก่อน Thread ที่สองเริ่มต้นทำงานนั่นเอง
อีกตัวอย่างสำหรับการใช้งานเมธอด join()
คือเมื่อเราต้องการรอผลลัพธ์จากหลาย Thread ก่อนที่จะทำงานต่อ ต่อไปมาดูตัวอย่างโปรแกรมเรียงตัวเลขในลิสต์ โดยใช้ความสามารถจาก Thread เข้ามาช่วย นี่เป็นโค้ดของโปรแกรม
from threading import Thread
class BubbleSorting(Thread):
def __init__(self, numbers):
Thread.__init__(self)
self.numbers = numbers
def run(self):
n = len(self.numbers)
for i in range(0, n):
for j in range(0, n - i - 1):
if self.numbers[j] > self.numbers[j+1]:
temp = self.numbers[j]
self.numbers[j] = self.numbers[j + 1]
self.numbers[j + 1] = temp
# List of numbers to sort
numbers = [8, 14, 4, 2, 1, 17, 12, 3, 0, 4, 16, 11]
# Slice list into half and supply to each thread
thr1 = BubbleSorting(numbers[0:6])
thr2 = BubbleSorting(numbers[6:12])
thr1.start()
thr2.start()
# Wait for all threads to complete
thr1.join()
thr2.join()
# Obtain sorted lists from threads
list1 = thr1.numbers
list2 = thr2.numbers
len1 = len(list1)
len2 = len(list2)
# Merge sorted lists to final list
sorted_numbers = []
i = j = 0
while i < len1 and j < len2:
if list1[i] <= list2[j]:
sorted_numbers.append(list1[i])
i += 1
else:
sorted_numbers.append(list2[j])
j += 1
while (i < len1):
sorted_numbers.append(list1[i])
i += 1
while (j < len2):
sorted_numbers.append(list1[j])
j += 1
print("Sorted from Thread-1:")
print(list1)
print("Sorted from Thread-2:")
print(list2)
print("Final sorted:")
print(sorted_numbers)
และนี่เป็นผลลัพธ์การทำงานของโปรแกรม
Sorted from Thread-1:
[1, 2, 4, 8, 14, 17]
Sorted from Thread-2:
[0, 3, 4, 11, 12, 16]
Final sorted:
[0, 1, 2, 3, 4, 4, 8, 11, 12, 14, 16, 17]
ในตัวอย่าง เป็นโปรแกรมเรียงตัวเลขจากน้อยไปมาก โดยเราจะแบ่งลิสต์ของตัวเลขออกเป็นสองส่วนเท่าๆ กัน และส่งลิสต์เหล่านั้นเข้าไปจัดเรียงภายในคลาส BubbleSorting
ด้วยอัลกอริทึม Bubble sort หลังจากทีทุก Thread เรียงตัวเลขเสร็จแล้ว เราได้นำลิสต์ทั้งสองมารวมกันใน Thread หลัก ต่อไปจะเป็นการอธิบายการทำงานของโค้ดในแต่ละส่วน
class BubbleSorting(Thread):
def __init__(self, numbers):
Thread.__init__(self)
self.numbers = numbers
def run(self):
n = len(self.numbers)
for i in range(0, n):
for j in range(0, n - i - 1):
if self.numbers[j] > self.numbers[j+1]:
temp = self.numbers[j]
self.numbers[j] = self.numbers[j + 1]
self.numbers[j + 1] = temp
ในตอนแรกของโปรแกรม เราได้สร้างคลาส BubbleSorting
คลาสนี้มีหน้าที่รับเอาลิสต์ของตัวเลขและนำมาเรียงจากน้อยไปมากด้วยอัลกอริทึม Bubble sort ที่เรากำหนดการทำงานไว้ในเมธอด run()
numbers = [8, 14, 4, 2, 1, 17, 12, 3, 0, 4, 16, 11]
thr1 = BubbleSorting(numbers[0:6])
thr2 = BubbleSorting(numbers[6:12])
เรามีลิสต์ของตัวเลขที่ยังไม่ได้จัดเรียง และเราได้แบ่งลิสต์ออกเป็นสองลิสต์เท่าๆ กันและส่งเข้าไปยัง Thread เพื่อจัดเรียง หลังจากนั้นสั่งให้ Thread ทั้งสองเริ่มต้นทำงานด้วยเมธอด start()
thr1.join()
thr2.join()
list1 = thr1.numbers
list2 = thr2.numbers
เนื่องจากว่าเราต้องการนำผลลัพธ์จาก Thread ทั้งสองมารวมกัน ดังนั้นเราได้เรียกใช้งานเมธอด join()
เพื่อรอให้ Thread ทั้งสองเรียงตัวเลขให้เสร็จก่อน แล้วค่อยนำมาตัวเลขที่จัดเรียงแล้วออกมารวมกันอีกที
sorted_numbers = []
i = j = 0
while i < len1 and j < len2:
if list1[i] <= list2[j]:
sorted_numbers.append(list1[i])
i += 1
else:
sorted_numbers.append(list2[j])
j += 1
while (i < len1):
sorted_numbers.append(list1[i])
i += 1
while (j < len2):
sorted_numbers.append(list1[j])
j += 1
หลังจากที่เราได้ลิสต์จากทั้งสอง Thread ที่จัดเรียงแล้ว เราสามารถนำลิสต์ที่เรียงแล้วนั้นมา Merge เข้าด้วยกันได้
Time Complexity เนื่องจากการเรียงตัวเลขจากลิสต์นั้นใช้เวลา O(n^2) เมื่อ n คือจำนวนของสมาชิกภายในลิสต์ ดังนั้นจากการลดขนาดของลิสต์ออกเป็นครึ่งหนึ่งทำให้เราลดเวลาเหลือเพียง O(n/2^2) ซึ่งถือว่าเร็วขึ้นมากๆ เนื่องจากการนำลิสต์ที่จัดเรียงแล้วมาเรียงกันใช้เวลาเพียง O(n) คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับเรื่องนี้ได้ในวิชาอัลกอริทึมในหัวข้อ Time Complexity
จากตัวอย่างก่อนหน้า เป็นเพียงการแสดงวิธีการใช้เมธอด join()
เพื่อรอผลลัพธ์จากทั้งสอง Thread ให้ทำงานเสร็จเท่านั้น อย่างไรก็ตามในการเรียงข้อมูลในลิสต์นั้นคุณสามารถใช้เมธอด sort()
ได้ ซึ่งนี่จะได้ผลลัพธ์เหมือนกับโปรแกรมด้านบน
# Achieve the same result
numbers = [8, 14, 4, 2, 1, 17, 12, 3, 0, 4, 16, 11]
print("Initial list")
print(numbers)
numbers.sort()
print("Sorted list")
print(numbers)
Thread synchronization
Thread synchronization คือการที่ Thread ตั้งแต่สอง Thread ขึ้นไปเข้าถึงขอบเขตของโปรแกรมบางส่วนพร้อมกันๆ ในเวลาเดียวกัน ยกตัวอย่างเช่น การเข้าถึงตัวแปรตัวเดียวกัน ซึ่งขอบเขตดังกล่าวนั้นเรียกว่า Critical section ซึ่งเมื่อเกิดเหตุการณ์นี้ขึ้นอาจทำให้โปรแกรมทำงานผิดพลาดหรือไม่ได้ผลลัพธ์อย่างที่คาดหวังได้ ดังนั้นในภาษา Python มีคลาสสำหรับจัดการเกี่ยวกับ Thread synchronization เช่น Lock
, RLock
และ Semaphor
ซึ่งคุณจะได้เรียนรู้ในบทต่อไป
ในบทนี้ เราได้ครอบคลุมเกี่ยวกับการใช้งาน Thread ในภาษา Python เบื้องต้น ในตอนนี้คุณสามารถใช้ประโยชน์จาก Thread เพื่อให้โปรแกรมทำงานแบบคู่ขนานกันได้