Thread ในภาษา Python

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

Thread คืออะไร

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

Thread และ Process ในภาษา Python

รูปภาพแสดงการทำงานของ Thread และ Process

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

การสร้าง Thread ในภาษา Python

ในการสร้าง Thread นั้นเราสามารถสร้างได้จากคลาส Thread ที่อยู่ภายในโมดูล threading ซึ่งโมดูลนี้ประกอบไปด้วยคลาสต่างๆ ที่ใช้สำหรับสร้างและทำงานเกี่ยวกับ Thread นี่เป็นตัวอย่างการสร้าง Thread ในภาษา Python

thread1.py
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 ดังในตัวอย่างต่อไปนี้

thread2.py
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 นี่เป็นตัวอย่าง

thread3.py
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

thread4.py
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 แรกทำงานเสร็จก่อน มาดูตัวอย่าง

thread_join1.py
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 เข้ามาช่วย นี่เป็นโค้ดของโปรแกรม

thread_join2.py
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 เพื่อให้โปรแกรมทำงานแบบคู่ขนานกันได้