ปกติเวลาที่รันโปรแกรมในไพธอนโปรแกรมจะทำตามคำสั่งตามลำดับทีละขั้นตอนโดยตลอดโปรแกรมถือว่าเป็นงานงานเดียวที่รันใน CPU ตัวเดียว
การที่โปรแกรมรันไปตามลำดับแบบนี้มีข้อดีคือเป็นระบบระเบียบ แต่ว่าในบางครั้งเราอาจจะมีงานบางอย่างที่ไม่จำเป็นต้องรอให้อีกงานนึงจบก่อนก็ได้ กรณีแบบนี้ถ้าสามารถรันโปรแกรมให้มันทำอะไรหลายๆไปพร้อมๆกันได้ก็จะทำให้การทำงานเร็วขึ้นมาก
ปกติแล้วคอมพิวเตอร์มีความสามารถที่จะทำงานหลายๆอย่างในเวลาเดียวกันได้อยู่แล้ว แต่ปัญหาคือหากเขียนโปรแกรมไพธอนด้วยวิธีทั่วไปยังไงก็รันได้ทีละงานในเวลาเดียว หากอยากรันหลายงานก็ต้องมาสั่งหลายๆครั้งเอง เช่นเปิดหน้าคอมมานด์ไลน์มาหลายๆอันแล้วรันไปทีละอัน แต่แบบนั้นต้องมาคอยเปิดหลายอัน อีกทั้งหากทำแบบนั้นก็จะเป็นการรันคนละโปรแกรม ไม่สามารถเอามาเชื่อมโยงกันได้โดยตรง
ยังดีที่ไพธอนมีเตรียมคำสั่งที่ใช้สำหรับรันหลายงานในเวลาเดียวกันได้โดยอัตโนมัติในโปรแกรมเดียว คำสั่งนั้นอยู่ในมอดูลชื่อ multiprocessing เป็นมอดูลติดตัวที่มีอยู่ในไพธอนอยู่แล้ว สามารถเรียกใช้ได้เลย
การใช้งาน ขอเริ่มด้วยการยกตัวอย่างการทำงานอย่างง่าย
ผลที่ได้
จะเห็นว่าขั้นตอนการใช้ก็คือ ให้นิยามฟังก์ชันของงานที่เราต้องการให้มันทำ จากนั้นให้สร้างอ็อบเจ็กต์ multiprocessing.Process ขึ้น (ในที่นี้ย่อเป็น mp.) โดยใส่ค่า target เป็นตัวฟังก์ชันของงานตัวนั้น จากนั้นก็ใช้เมธอด .start() จะเป็นการเริ่มทำงานในฟังก์ชันนั้นทันที
สำหรับ if(__name__=='__main__'): ในที่นี้เป็นธรรมเนียมปฏิบัติที่มักจะต้องเขียนประจำเวลาใช้งาน multiprocessing เพื่อให้คำสั่งนี้ถูกเรียกใช้เฉพาะเวลาที่ถูกรันโดยตรง ไม่ใช่ถูกเรียกในฐานะมอดูล ซึ่งจะป้องกันปัญหาที่อาจเกิดขึ้นได้
เกี่ยวกับเรื่องรูปแบบการเขียนแบบนี้มีเขียนไว้ใน
https://phyblas.hinaboshi.com/tsuchinoko35 ซึ่งผลที่ได้จะเห็นว่าข้อความสุดท้ายใน main ถูกพิมพ์ออกมาก่อนทั้งๆที่สั่ง p.start() ไปแล้ว นั่นเพราะหลังจากสั่งเริ่มงานไปใน job จะเจองานที่ต้องใช้เวลาทำให้คำสั่ง print ตัวหลังมาช้าลงไป แต่ คำสั่ง print ใน main ที่ตามมานั้นต่อมาทันที
นี่แสดงให้เห็นว่าคำสั่งที่ตามต่อมาหลังจากสั่งเริ่มงานไปแล้วนั้นมันไม่ได้รอให้งานเสร็จก่อนจึงทำงาน งานถูกสั่งทิ้งไว้แล้วโปรแกรมหลักก็ไปทำอะไรอย่างอื่นต่อได้ทันที
กรณีที่ฟังก์ชันนั้นต้องการอาร์กิวเมนต์ ให้ใส่ลงในค่า args แทนที่จะเรียก job(1,2) แบบฟังก์ชันทั่วไป คือเขียนแบบนี้
จากนั้นลองดูกรณีที่มีการรัน ๒ งานพร้อมกัน เช่นแบบนี้
รันแล้วจะได้
ในที่นี้มีการใช้คำสั่ง time.sleep นี่เป็นคำสั่งสำหรับทำให้โปรแกรมพักการทำงานตามเวลาที่กำหนด หน่วยเป็นวินาที ที่ใส่ลงไปก็เพื่อให้เห็นความแตกต่างของลำดับการทำงานชัดเจน
ผลที่ได้จะเห็นว่าทั้งสองงานถูกทำไปพร้อมๆกัน จึงเห็นค่าถูกพิมพ์ออกมาสลับกันไปแบบนี้
หากต้องการรันหลายงานพร้อมกันก็อาจเขียนแบบนี้ได้
ผลที่ได้
จะเห็นว่าแต่ละรอบของทุกงานทำเสร็จก่อนจึงเริ่มงานของรอบถัดไป แสดงให้เห็นว่าทุกงานทำไปพร้อมๆกัน ไม่ได้รองานนึงเสร็จค่อยทำอีกงาน
การใช้ join หากมีคำสั่งบางอย่างที่ต้องการรอให้งานที่สั่งไปเสร็จก่อนแล้วค่อยทำ จำเป็นจะต้องใช้เมธอด join
ตัวอย่างการใช้
ผลที่ได้
จะเห็นว่า print ที่เขียนก่อน join จะทำงานก่อน แต่ print ที่อยู่หลัง join จะรอให้ทำงานจบ
หากทุกงานที่สั่งไปมีการใช้ join โปรแกรมก็จะรอให้ทุกงานเสร็จทั้งหมดก่อน
การใช้ queue ปกติแล้วค่าตัวแแปรต่างๆที่อยู่ภายในฟังก์ชันของงานนั้นไม่สามารถที่จะคืนกลับมายังโปรแกรมหลักได้ เพราะเราไม่ได้เขียนคำสั่งในรูปแบบของ x = job() แบบนี้ แต่เวลารันเราเริ่มด้วยคำสั่ง p.start() ต่อให้เขียน return ไปก็ไม่ได้อะไรกลับมา เช่น
ได้
ดังนั้นต้องใช้วิธีอื่นในการที่จะได้ตัวแปรกลับออกมาจากงานที่ทำ วิธีหนึ่งที่สามารถใช้ได้ก็คือใช้ Queue
สามารถเขียนได้แบบนี้
ก่อนอื่นตอนนิยามฟังก์ชันของงานต้องใส่ตัวแปรนึงสำหรับรับค่าไปด้วย จากนั้นใช้เมธอด put เพื่อป้อนค่าที่ต้องการให้คืนกลับไป
แล้วในโปรแกรมหลักให้สร้างตัวแปรนึงขึ้นมาเป็นออบเจ็กต์ของ mp.Queue แล้วป้อนใส่มันลงไปในอาร์กิวเมนต์ของฟังก์ชันด้วย จากนั้นก็สามารถเอาค่าที่คืนกลับมาได้โดยใช้เมธอด get
จะใช้ put กี่ครั้งก็ได้ในฟังก์ชัน แล้วเวลา get ก็จะได้ค่าออกมาทีละค่า เรียงตามลำดับ
ได้
เพียงแต่ข้อควรระวังคือมีการใช้ put กี่ครั้งก็ต้อง get เป็นจำนวนครั้งเท่านั้น หาก get เกินโปรแกรมจะหยุดทำงานไปเลย
นั่นเพราะถ้าเราใช้ get เมื่อไหร่โปรแกรมหลักจะหยุดรอให้มีการ put เกิดขึ้นในฟังก์ชันจึงรับค่ามาแล้วทำงานต่อ ไม่งั้นโปรแกรมจะไม่เดินต่อไป
กรณีที่ส่งค่า q ไปแล้วแต่ไม่มีการ put สักที เช่นแบบนี้ โปรแกรมจะรอไปตลอดกาล ไม่ขยับไปไหน
ดังนั้นจึงต้องระวังด้วย เวลาใช้วิธีนี้
กรณีที่รันสองงานขึ้นไปพร้อมกันโดยใช้ q ตัวเดียวกัน ค่าที่ได้จะเรียงตามลำดับว่าใคร put ก่อนกัน ไม่ได้อยู่ที่ใครเริ่มต้นก่อน เช่น
จะได้
นั่นเพราะว่า p2 สั่งให้รอแค่ 2 วินาที แต่ p1 รอ 3 วินาที ดังนั้น p2 จึงเสร็จก่อน ใส่ค่าลงใน q ก่อน
ดังนั้นควรจะเข้าใจลำดับข้อมูลให้ดี ไม่เช่นนั้นจะได้ค่าออกมาผิด
ตัวแปรร่วมสำหรับใช้ในทุกงาน ปกติแล้วหากเราประกาศตัวแปรไว้นอกฟังก์ชัน ตัวแปรนั้นสามารถเปลี่ยนแปลงค่าได้จากภายในฟังก์ชันโดยใช้คำสั่ง global (ใครไม่แม่นอ่านทวนได้ใน
https://phyblas.hinaboshi.com/tsuchinoko19)
เช่นมีฟังก์ชันแบบนี้
ถ้าเรียกใช้โดยทั่วไปแบบนี้
จะได้
นั่นคือลิสต์ c ค่อยๆถูกเติม
แต่หากเรียกด้วย Process แบบนี้
ผลที่ได้
จะเห็นว่าถึง p1 รันก่อนแล้วเติมค่าให้ลิสต์ไป แต่พอ p2 เรียกใช้อีกกลับอยู่ในสภาพก่อนเติมแล้วจึงเติมใหม่อีก แสดงว่าตัวแปรนี้ไม่สามารถใช้ร่วมกันได้
สำหรับกรณีแบบนี้ การกำหนดตัวแปรสากลที่ใช้ร่วมจะต้องทำโดยใช้ mp.Value
ตัวอย่าง
ได้
การใช้ Value แบบนี้ต้องกำหนดชนิดของตัวแปรไปด้วย โดยใส่ชนิดตัวแปรก่อนค่อยตามด้วยค่า
ชนิดของข้อมูลจะใช้ตัวย่อ อ้างอิงตามนี้
https://docs.python.org/3/library/array.html ในที่นี้ i หมายถึงตัวแปรจำนวนเต็ม
ค่าของตัวแปรจะไม่ได้อยู่ที่ตัวแปรโดยตรงแต่อยู่ที่แอตทริบิวต์ .value ดังนั้นจึงต้องพิมพ์ .value ต่อท้ายเวลาใช้อย่างที่เห็น
หากต้องการเป็นตัวแปรหลายตัวก็ต้องทำเป็นอาเรย์ โดยใช้ mp.Array
ได้
อาเรย์ในที่นี้นั้นจะคล้ายๆกับอาเรย์ในภาษา C หรือ ndarray ของ numpy คือสามารถแก้ไขค่าภายในได้แต่ไม่สามารถเพิ่มหรือลดสมาชิกได้ นอกจากสร้างใหม่ไปเลย
การใช้ Pool Pool นั้นเป็นอีกวิธีที่ใช้สั่งงานเช่นเดียวกันกับ Process เพียงแต่สามารถสั่งให้คืนค่าออกมาได้โดยตรง ถือเป็นอีกวิธีหนึ่งที่จะสามารถนำค่าที่คืนกลับจากงานที่สั่งไปมาใช้ได้
การใช้งานจะคล้ายกับ Process โดยมีวิธีการเขียนดังนี้
ได้
เริ่มจากสร้างออบเจ็กต์ของ Pool ขึ้นมา จากนั้นก็สั่งให้เริ่มทำงานโดยใช้เมธอด apply_async ใส่ฟังก์ชันแล้วตามด้วยอาร์กิวเมนต์
ถ้าเจอเมธอด get โปรแกรมก็จะทำการรอให้ฟังก์ชันทำงานจนจบแล้วคืนค่ากลับมา ไม่เช่นนั้นจะไม่เดินต่อ
ค่า processes ที่ใส่เข้าไปตอนสร้าง Pool นี้คือจำนวนที่จะให้มันทำงานสูงสุดในเวลาเดียวกัน ถ้าเราสั่งงานให้ pool นั้นไปมากกว่าจำนวนที่กำหนดมันก็จะรอให้งานที่เริ่มทำก่อนนั้นจบลงไปก่อน เช่น
ได้
จะเห็นว่างานที่ 1 และ 2 ทำพร้อมกัน แล้วต่อมางานที่ 3 และ 4 ก็ทำงานพร้อมกัน แล้วจึงเริ่มงานที่ 5 นั่นเพราะสั่งให้ทำงานสูงสุดแค่ 2
นอกจากใช้ apply_async แล้ว ยังมีอีกเมธอดที่สะดวกที่มักใช้ใน Pool คือ map
map ใช้สั่งงานโดยรับค่าเข้าไปหลายตัวพร้อมกันแล้วคืนค่าของแต่ละงานกลับมาเป็นลิสต์
ได้
ดูเผินๆอาจคล้ายกับใช้ฟังก์ชัน map ธรรมดา
ผลที่ได้ก็เหมือนกัน เพียงแต่ว่าการสั่งด้วย Pool แบบนี้งานทั้งหมดจะทำไปพร้อมกันในเวลาเดียวกัน จึงเร็วขึ้นมาก
สถานะการทำงานของโปรแกรม สุดท้ายนี้ลองมาดูตรงนี้สักหน่อย
หากขณะที่โปรแกรมทำงานอยู่เราไปดูที่สถานะการทำงานของโปรแกรมเช่นผ่าน
ตัวจัดการงาน (task manager) ของ windows หรือ
ตัวตรวจสอบกิจกรรม (activity monitor) ของ mac ก็จะเห็นได้ว่ามันมีงาน python โผล่มาเต็มไปหมด แยกกันชัดเจน เช่นลองรันโค้ดนี้
จากนั้นจะเห็นว่าเครื่องทำงานทั้งหมดตามที่สั่งไปอย่างเต็มที่ แน่นอนว่าไม่ใช่ว่าสั่งไปกี่งานก็จะเร็วขึ้นเท่านั้น แต่ CPU ของแต่ละเครื่องก็มีขีดจำกัดอยู่ ถ้าเครื่องทำงานเต็มที่แล้วก็จะทำให้ทุกอย่างช้าลงตาม
ดังนั้นควรจะตรวจดูสถานะการทำงานของโปรแกรมในเครื่องเพื่อให้ใช้ได้อย่างมีประสิทธิภาพสูงสุดและไม่ให้ทำงานหนักเกินไปโดยเปล่าประโยชน์
อ้างอิง