Threads ในภาษา Ruby

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

  • การสร้าง Thread ในภาษา Ruby
  • Thread Joining
  • Multi-Threaded Programming
  • Thread Synchronization
  • การสร้างคลาส Thread

Thread คืออะไร

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

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

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

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

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

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

thread.rb
thr1 = Thread.new {
    puts "Hello from Thread"
}

thr2 = Thread.new("Mateo") { |name|
    puts "Hello #{name} from Thread"
}

puts "Main Thread"

thr1.join
thr2.join

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

หรือคุณอาจจะต้องการส่งหลายค่าเข้าไปยัง Thread โดยการเรียกใช้งานเมธอด new เหมือนกับในรูปแบบด้านล่างนี้

Thread.new(val1, val2, ...) { |arg1, arg2, ...|
    ...
}

ทันทีที่เราสร้าง Thread มันจะเริ่มทำงานในทันที และในภาษา Ruby อย่างหนึ่งเกี่ยวกับ Thread ที่คุณจะต้องจำไว้ก็คือ Thread หลักนั้นจะไม่รอให้ Thread ย่อยทำงานให้เสร็จก่อน และเนื่องจากทุก Thread นั้นทำงานไปพร้อมกัน ดังนั้นในบางครั้ง Thread หลักอาจจะทำงานเสร็จก่อน และเมื่อ Thread หลักทำงานเสร็จ มันจะส่งผลให้ Thread ย่อยหยุดทำงานในทันที

thr1.join
thr2.join

ดังนั้น เราสามารถเรียกใช้งานเมธอด join เพื่อรอให้ Thread ย่อยทำงานให้เสร็จก่อนได้ ซึ่งเมื่อ Ruby พบกับการเรียกใช้งานเมธอดนี้ มันจะรอให้ Thread ทำงานเสร็จก่อน ก่อนที่จะทำงานในบรรทัดต่อไป

Main Thread
Hello from Thread
Hello Mateo from Thread

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

Thread Joining

ในภาษา Ruby เราสามารถรวม Thread ได้ด้วยเมธอด join การรวม Thread จะบล็อคการทำงานใน Thread หลักและรอจนกว่า Thread ย่อยจะทำงานเสร็จ ก่อนที่โปรแกรมจะทำงานในบรรทัดต่อไปได้ นั่นทำให้การทำงานของโปรแกรมเป็นแบบตามลำดับนั่นเอง การรวม Thread นั้นมีประโยชน์ในการกรณีที่เราต้องการทำงานอย่างแรกก่อน และทำบางอย่างในทีหลัง มาดูตัวอย่าง

thread_joining.rb
thr1 = Thread.new {
    for i in (1..10)
        puts "Thread 1: #{i}"
        sleep(0.1)
    end
}

thr2 = Thread.new {
    for i in (1..10)
        puts "Thread 2: #{i}"
        sleep(0.1)
    end
}

thr1.join
thr2.join

thr3 = Thread.new {
    for i in (1..10)
        puts "Thread 3: #{i}"
    end
}

thr3.join

puts "Main Thread"

ในตัวอย่าง เป็นโปรแกรมสำหรับแสดงตัวเลข 1 - 10 ออกทางหน้าจอ เราได้แสดงผลตัวเลขพร้อมกับชื่อของ Thread เพื่อทำให้คุณสามารถเห็นภาพได้ชัดเจนขึ้น

thr1 = Thread.new {
    for i in (1..10)
        puts "Thread 1: #{i}"
        sleep(0.1)
    end
}

thr2 = Thread.new {
    for i in (1..10)
        puts "Thread 2: #{i}"
        sleep(0.1)
    end
}

ในตอนแรกของโปรแกรม เราได้สร้าง Thread ขึ้นมาสอง Thread ในตัวแปร thr1 และ thr2 ทั้งสอง Thread นี้ทำงานเหมือนกันคือแสดงตัวเลขจาก 1 - 10 ออกทางหน้าจอ ในการแสดงตัวเลขแต่รอบในคำสั่ง for loop เราได้ใช้เมธอด sleep เพื่อหน่วงการทำงานให้ช้าลงเป็นเวลา 0.1 วินาที นั่นเป็นเพราะว่าคอมพิวเตอร์ทำงานเร็วมาก เราทำเช่นนี้เพื่อให้คุณเห็นว่าทั้งสอง Thread นั้นทำงานไปพร้อมๆ กัน

thr1.join
thr2.join

หลังจากนั้นเราเรียกใช้เมธอด join จากออบเจ็คของ Thread ทั้งสอง นี่หมายความว่าเราต้องการรอให้ Thread ทั้งสองทำงานเสร็จก่อน ก่อนที่จะทำงานในบรรทัดต่อไป

thr3 = Thread.new {
    for i in (1..10)
        puts "Thread 3: #{i}"
    end
}

thr3.join

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

puts "Main Thread"

สุดท้ายเป็นคำสั่งที่ทำงานใน Thread หลัก คำสั่งนี้จะทำงานท้ายสุดหลังจากที่ทุก Thread ย่อยทำงานเสร็จหมดแล้ว นั่นเป็นเพราะการใช้งานเมธอด join เพื่อรอให้ Thread อื่นๆ ทำงานให้เสร็จก่อนนั่นเอง

Thread 1: 1
Thread 2: 1
Thread 1: 2
Thread 2: 2
Thread 2: 3
Thread 1: 3
Thread 2: 4
Thread 1: 4
Thread 2: 5
Thread 1: 5
Thread 2: 6
Thread 1: 6
...

นี่เป็นผลลัพธ์การทำงานของโปรแกรม โปรแกรมเริ่มทำงานจากสอง Thread แรกก่อน หลังจากนั้นทำงาน Thread ที่สาม และสุดท้ายทำงานในคำสั่งของ Thread หลัก

Multi-Threaded Programming

การเขียนโปรแกรมแบบ Multi-Threaded คือการใช้ประโยชน์จากการทำงานพร้อมกันของ Thread เพื่อแบ่งงานออกเป็นส่วนย่อยๆ เพื่อให้แต่ละ Thread ช่วยกันทำงาน นั่นจะส่งผลให้โปรแกรมของเราสามารถทำงานได้รวดเร็วขึ้น

ตัวอย่างต่อมาจะเป็นการใช้ประโยชน์จากเขียนโปรแกรมแบบ Multi-Threaded เราจะเขียนโปรแกรมสำหรับคำนวณเกรดและนับเกรดของนักเรียนจำนวน 40 คน เนื่องจากการคำนวณเกรดของนักเรียนแต่ละคนนั้นไม่ขึ้นต่อกัน ดังนั้นเราสามารถใช้ Thread เพื่อแบ่งการทำงานนี้ได้ นี่เป็นโค้ดของโปรแกรม

multi_threaded.rb
def cal_grade(score)
    if score >= 80
        "A"
    elsif score >= 70
        "B"
    elsif score >= 60
        "C"
    elsif score >= 50
        "D"
    else
        "F"
    end
end

score1 = [54, 84, 6, 72, 100, 35, 92, 15, 73, 21, 43, 72, 4, 4, 100, 29, 49, 25, 61, 75]
score2 = [31, 39, 83, 42, 18, 20, 2, 47, 6, 34, 83, 98, 36, 7, 100, 12, 87, 69, 92, 42]

thr1 = Thread.new(score1) { |scores|
    hash = {}
    scores.each { |item|
        grade = cal_grade(item)
        if hash.key?(grade)
            hash[grade] = hash[grade] + 1
        else
            hash[grade] = 1
        end 
    }
    hash
}

thr2 = Thread.new(score2) { |scores|
    hash = {}
    scores.each { |item|
        grade = cal_grade(item)
        if hash.key?(grade)
            hash[grade] = hash[grade] + 1
        else
            hash[grade] = 1
        end 
    }
    hash
}

thr1.join
thr2.join

hash1 = thr1.value
hash2 = thr2.value

puts "Data computed from each threads"
p hash1
p hash2

sum_hash = {}
["A", "B", "C", "D", "F"].each { |key|
    sum_hash[key] = hash1.fetch(key, 0) + hash2.fetch(key, 0) 
}

puts "Studies result of 40 students"
sum_hash.each_pair { |key, value|
    puts "Grade #{key}: #{value} students"
}

ในตัวอย่าง เป็นโปรแกรมสำหรับคำนวณเกรดและนับว่าจากนักเรียนจำนวน 40 คนนั้นแบ่งออกเป็นเกรดต่างๆ จำนวนกี่คน นั่นหมายความว่าเราต้องหาว่านักเรียนแต่ละคนได้เกรดอะไรจากคะแนนของพวกเขา หลังจากนั้นก็นับจำนวนเกรดทั้งหมดที่ได้ว่ามีเกรดละกี่คน

def cal_grade(score)
    if score >= 80
        "A"
    elsif score >= 70
        "B"
    elsif score >= 60
        "C"
    elsif score >= 50
        "D"
    else
        "F"
    end
end

ในตอนแรกของโปรแกรม เราได้สร้างเมธอด cal_grade สำหรับคำนวณเกรดจากคะแนนที่รับเข้ามา โดยมีเงื่อนไขว่า ถ้าคะแนนมากกว่าหรือเท่ากับ 80 จะได้เกรด A ถ้าคะแนนมากกว่าหรือเท่ากับ 70 จะได้เกรด B ถ้าคะแนนมากกว่าหรือเท่ากับ 60 จะได้เกรด C ถ้าคะแนนมากกว่าหรือเท่ากับ 50 จะได้เกรด D และถ้าหากต่ำกว่า 50 จะได้เกรด F

score1 = [54, 84, 6, 72, 100, 35, 92, 15, 73, 21, 43, 72, 4, 4, 100, 29, 49, 25, 61, 75]
score2 = [31, 39, 83, 42, 18, 20, 2, 47, 6, 34, 83, 98, 36, 7, 100, 12, 87, 69, 92, 42]

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

thr1 = Thread.new(score1) { |scores|
    hash = {}
    scores.each { |item|
        grade = cal_grade(item)
        if hash.key?(grade)
            hash[grade] = hash[grade] + 1
        else
            hash[grade] = 1
        end 
    }
    hash
}

นี่เป็น Thread สำหรับคำนวณเกรดและนับว่าแต่ละเกรดนั้นมีนักเรียนจำนวนเท่าไหร่ที่ได้ บล็อครับค่าเป็นอาเรย์ของคะแนนที่ส่งเข้ามาและนำคะแนนไปคำนวณเกรดโดยเรียกใช้เมธอด cal_grade หลังจากที่ได้รับผลของเกรดแล้ว เรานับจำนวนของคนที่ได้แต่ละเกรดโดยใช้ Hash ในการนับและใช้ชื่อเกรดเป็น Key ของ Hash

ในตอนท้ายของบล็อค เราส่งค่ากลับจากบล็อคเป็น Hash ของจำนวนนักเรียนที่นับได้สำหรับแต่ละเกรด นี่จะทำให้เราสามารถรับเอาค่าดังกล่าวนี้ได้จากเมธอด value ของ Thread

thr1.join
thr2.join

hash1 = thr1.value
hash2 = thr2.value

หลังจากเราสร้าง Thread ทั้งสองและเรียกใช้งานโดยการส่งคะแนนเข้าไปใน Thread แล้ว เรารอให้ Thread ทั้งสองทำงานให้เสร็จด้วยเมธอด join หลังจากนั้นเราใช้เมธอด value สำหรับรับเอาค่าที่ส่งกลับมาจาก Thread

puts "Data computed from each threads"
p hash1
p hash2

เราได้แสดงจำนวนของนักเรียนที่นับได้จากทั้งสอง Thread ข้อมูลเหล่านี้เป็น Hash ที่นับว่ามีนักเรียนได้เกรดต่างๆ เป็นจำนวนเท่าไหร่ โดยใช้ชื่อของเกรดเป็น Key ของ Hash

sum_hash = {}
["A", "B", "C", "D", "F"].each { |key|
    sum_hash[key] = hash1.fetch(key, 0) + hash2.fetch(key, 0) 
}

puts "Studies result of 40 students"
sum_hash.each_pair { |key, value|
    puts "Grade #{key}: #{value} students"
}

มีอีกอย่างหนึ่งที่เราต้องทำก็คือรวมจำนวนนักเรียนที่นับได้จากทั้งสอง Thread เข้าด้วยกัน เราได้วนอ่านค่า Key ของ Hash ทั้งสองและนับค่าดังกล่าวมารวมไว้ใน Hash ใหม่ที่เราสร้างขึ้นชื่อว่า sum_hash และแสดงผลออกมาว่าแต่ละเกรดนั้นมีจำนวนนักเรียนเท่าไหร่

Data computed from each threads
{"D"=>1, "A"=>4, "F"=>10, "B"=>4, "C"=>1}
{"F"=>13, "A"=>6, "C"=>1}
Studies result of 40 students
Grade A: 10 students
Grade B: 4 students
Grade C: 2 students
Grade D: 1 students
Grade F: 23 students

นี่เป็นผลลัพธ์การทำงานของโปรแกรมจากการนับว่านักเรียนได้เกรดต่างๆ เป็นจำนวนเท่าไหร่จากคะแนนทั้งหมดที่เรามี

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

Thread Synchronization

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

ในภาษา Ruby เราสามารถใช้คลาส Mutex ในการสร้างตัวส่งสัญญาณระหว่าง Thread ได้ คลาสนี้ถูกออกแบบมาสำหรับใช้ประสานงานในการเข้าถึงข้อมูลร่วมกันระหว่าง Thread ที่ใช้งานง่ายและตรงไปตรงมา มาดูตัวอย่าง

synchronization.rb
def is_prime(n)
    if n == 1
        return false
    end
    for i in (2...n)
        return false if n % i == 0 && i != n
    end
    return true
end

semaphore = Mutex.new
counter = 1

thr1 = Thread.new {
    while counter <= 100
        n = nil
        semaphore.synchronize {
            n = counter
            counter = counter + 1
        }       
        if is_prime(n)
            puts "Thread 1: #{n} is prime"
        else
            puts "Thread 1: #{n} is not prime"
        end
        sleep(0.1)
    end
}

thr2 = Thread.new {
    while counter <= 100
        n = nil
        semaphore.synchronize {
            n = counter
            counter = counter + 1
        }
        if is_prime(n)
            puts "Thread 2: #{n} is prime"
        else
            puts "Thread 2: #{n} is not prime"
        end
        sleep(0.1)
    end
}

thr1.join
thr2.join

ในตัวอย่าง เป็นโปรแกรมสำหรับแสดงตัวเลขจำนวนเฉพาะระหว่าง 1 - 100 เราต้องการทราบว่ามีตัวเลขไหนที่เป็นจำนวนเฉพาะบ้าง เนื่องจากการตรวจสอบจำนวนเฉพาะในแต่ละตัวเลขนั้นใช้เวลาแตกต่างกัน ตัวเลขที่ใหญ่กว่ามีแนวโน้มว่าจะใช้เวลานานกว่า ดังนั้นเราจะไม่แบ่งข้อมูลออกเป็นสองส่วนเหมือนกับในตัวอย่างก่อนหน้า แต่เราจะใช้วิธีให้แต่ละ Thread ค่อยๆ นำตัวเลขไปคำนวณที่ละตัวแทน

def is_prime(n)
    if n == 1
        return false
    end
    for i in (2...n)
        return false if n % i == 0 && i != n
    end
    return true
end

ในตอนแรก เราสร้างเมธอด is_prime สำหรับหาว่าตัวเลขเป็นจำนวนเฉพาะหรือไม่ เมธอดนี้ส่งค่ากลับเป็น Boolean ว่าตัวเลขเป็นจำนวนเฉพาะหรือไม่ จำนวนเฉพาะคือจำนวนที่มีแค่หนึ่งและตัวมันเองเท่านั้นที่สามารถหารลงตัวได้

semaphore = Mutex.new
counter = 1

หลังจากนั้นเราสร้างออบเจ็ค semaphore ซึ่งเป็นออบเจ็คจากคลาส Mutex สำหรับควบคุมการเข้าถึงตัวแปรที่ใช้ร่วมกัน เราได้ประกาศตัวแปร counter ที่ใช้สำหรับเก็บค่าตัวเลขที่ถูกประมวลผลไปแล้ว ตัวแปรนี้จะถูกเข้าถึงจากทั้งสอง Thread ดังนั้นเราจะใช้ออบเจ็ค semaphore เพื่อควบคุมให้ทีละ Thread สามารถอ่านค่าและเปลี่ยนแปลงค่าในตัวแปรได้นั่นเอง ซึ่งกระบวนการนี้เองเรียกว่า Synchronization

thr1 = Thread.new {
    while counter <= 100
        n = nil
        semaphore.synchronize {
            n = counter
            counter = counter + 1
        }       
        if is_prime(n)
            puts "Thread 1: #{n} is prime"
        else
            puts "Thread 1: #{n} is not prime"
        end
        sleep(0.1)
    end
}

ต่อมาเป็นการทำงานของแต่ละ Thread ซึ่งจะวนอ่านค่าตัวเลขจากตัวแปร counter มาคำนวณเพื่อหาว่าตัวเลขเป็นจำนวนเฉพาะหรือไม่ เนื่องจากตัวแปร counter นั้นจะต้องเข้าถึงจากทั้งสอง Thread เราใช้เมธอด synchronize เพื่อทำให้แน่ใจว่ามีเพียงหนึ่ง Thread เท่านั้นที่สามารถเข้าถึงตัวแปรได้ในช่วงเวลานั้นๆ เราได้อ่านค่าเข้ามาเก็บไว้ในตัวแปร n เพื่อใช้งานใน Thread หลังจากนั้นเพิ่มค่าในตัวแปรขึ้นไป 1

if is_prime(n)
    puts "Thread 1: #{n} is prime"
else
    puts "Thread 1: #{n} is not prime"
end

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

sleep(0.1)

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

Thread 1: 1 is not prime
Thread 2: 2 is prime
Thread 1: 3 is prime
Thread 2: 4 is not prime
Thread 1: 5 is prime
Thread 2: 6 is not prime
Thread 1: 7 is prime
Thread 2: 8 is not prime
Thread 2: 9 is not prime
Thread 1: 10 is not prime
Thread 1: 11 is prime
Thread 2: 12 is not prime
Thread 1: 13 is prime
Thread 2: 14 is not prime
Thread 1: 15 is not prime
...

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

การสร้างคลาส Thread

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

my_thread.rb
class MyThread < Thread

    def initialize(name, count)
        @name = name
        @count = count
        super {
            puts "#{@name} started"
            counter
        }
    end

    def counter
        for i in (1..@count)
            puts "#{@name}: #{i}"
        end
    end

end

threads = [
    MyThread.new("Thread 1", 10),
    MyThread.new("Thread 2", 5),
    MyThread.new("Mateo", 5)
]

threads.each { |thr|
    thr.join
}

ในตัวอย่าง เราได้สร้างคลาสชื่อว่า MyThread คลาสนี้เป็นคลาส Thread เนื่องจากเราได้สืบทอดคลาสมาจากคลาส Thread ในคอนสตรัคเตอร์ของคลาสหรือเมธอด initialize รับอาร์กิวเมนต์สองตัวคือ name และ count สำหรับเก็บชื่อของ Thread และจำนวนตัวเลขที่ต้องการนับใน Thread ตามลำดับ

super {
    puts "#{@name} started"
    counter
}

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

def counter
    for i in (1..@count)
        puts "#{@name}: #{i}"
    end
end

เมธอด counter เป็นเมธอดสำหรับนับตัวเลขจาก 1 - @count ซึ่งจะเป็นค่าที่เราส่งเข้ามาในตอนสร้าง Thread ซึ่งแต่ละ Thread ที่สร้างจากคลาสนี้จะมีเมธอดนับเลขเป็นของมันเอง ซึ่งนี่จะทำให้เราไม่ต้องเขียนโค้ดซ้ำสำหรับแต่ละ Thread

threads = [
    MyThread.new("Thread 1", 10),
    MyThread.new("Thread 2", 5),
    MyThread.new("Mateo", 5)
]

threads.each { |thr|
    thr.join
}

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

Thread 1 started
Thread 1: 1
Thread 1: 2
Thread 1: 3
Thread 1: 4
Thread 1: 5
Thread 1: 6
Thread 1: 7
Thread 1: 8
Thread 1: 9
Thread 1: 10
Thread 2 started
Thread 2: 1
Thread 2: 2
Thread 2: 3
Thread 2: 4
Thread 2: 5
Mateo started
Mateo: 1
Mateo: 2
Mateo: 3
Mateo: 4
Mateo: 5

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

ในบทนี้ คุณได้เรียนรู้พื้นฐานเกี่ยวกับ Thread ในภาษา Ruby อย่างไรก็ตาม ยังมีเมธอดอีกเป็นจำนวนมากของ Thread ที่เรายังไม่ได้พูดถึง ยกตัวอย่างเช่น เมธอด exit ใช้สำหรับหยุดการทำงานของ Thread หรือเมธอด status ใช้สำหรับดูสถานะการทำงานของ Thread ถ้าหากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับ Thread คุณสามารถดูได้ที่ https://ruby-doc.org/core-2.7.1/Thread.html