แยกงานเป็น FreeRTOS task แล้วเร็วขึ้นเสมอไหม

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

หากงานยังเรียบง่าย การจัดเป็น state machine ในลูปที่ไม่รอค้างก็อาจเพียงพอ ไม่จำเป็นต้องเพิ่ม task เพื่อให้ดูเป็นระบบใหญ่ ส่วนเมื่อแยกแล้วก็ควรมีขอบเขตชัดว่างานใดรับผิดชอบอะไร จะได้ไม่ให้ทุก task เขียนค่าทุกอย่างร่วมกัน
ทำพร้อมกันได้เท่าไรขึ้นกับระบบที่ใช้
scheduler ช่วยจัดเวลาของ task ที่พร้อมทำงาน แต่จำนวน core และรายละเอียดการทำงานยังขึ้นกับชิปกับการตั้งค่า ไม่ใช่บอร์ดที่ใช้ชื่อ ESP32 ทุกตัวจะมีรูปแบบเดียวกัน และการสร้างหลาย task ก็ไม่ได้เพิ่มกำลังประมวลผลของฮาร์ดแวร์ขึ้นมา
สำหรับงานที่ส่วนใหญ่กำลังรอ การแบ่ง task อาจช่วยให้งานอื่นมีโอกาสเดินต่อได้ แต่ถ้าทุกงานกำลังคำนวณหนักหรือแย่งทรัพยากรเดียวกัน เราก็ยังต้องแบ่งกำลังที่มี รวมถึงมีค่าใช้จ่ายด้านหน่วยความจำและการจัดการเพิ่มขึ้น
การเลือก priority จึงควรมาจากหน้าที่และข้อกำหนดเวลา ไม่ใช่เพิ่มให้ task ที่ดูช้าแล้วหวังว่าจะหายเสมอ เพราะงาน priority สูงที่พร้อมวิ่งตลอดอาจทำให้งานต่ำกว่าไม่ค่อยมีโอกาสทำ จนปัญหาไปปรากฏที่ส่วนอื่นแทน
การผูก core ก็เป็นอีกการตัดสินใจที่ควรมีเหตุผลและตรวจจากผลจริง บางงานมีข้อจำกัดของมันอยู่แล้ว แต่การกระจายตามความรู้สึกโดยไม่ดูการรอหรือการใช้ทรัพยากรร่วม อาจเพิ่มความซับซ้อนโดยไม่ได้แก้จุดที่ทำให้รอ
มีข้อมูลร่วมแล้วต้องมีเจ้าของที่ชัด
สมมติงานอ่านเซนเซอร์กำลังเขียนโครงข้อมูล ในขณะที่หน้าจออ่านไปวาด หากไม่มีวิธีประสานกัน หน้าจออาจเห็นบางฟิลด์เป็นค่ารอบใหม่และบางฟิลด์เป็นรอบเก่า ถึงแต่ละฟังก์ชันจะอ่านเขียนได้ถูกตอนรันเดี่ยวก็ตาม
แนวทางหนึ่งคือให้งานอ่านเป็นเจ้าของข้อมูลแล้วส่งสำเนาที่ครบหนึ่งชุดผ่าน queue ผู้รับจะได้ใช้ตัวอย่างเดียวกันทั้งค่าและเวลา ไม่ไปแก้โครงที่อีกงานกำลังใช้อยู่ แต่ก็ต้องรู้ว่ารูปแบบ queue ที่เลือกคัดลอกข้อมูลอย่างไร
ถ้าส่ง pointer แทนข้อมูล ต้องดูอายุของหน่วยความจำและสิทธิ์แก้ไขด้วย การส่งตำแหน่งของตัวแปรชั่วคราวแล้วให้ผู้รับใช้ภายหลัง อาจกลายเป็นปัญหาที่เกิดเฉพาะบางจังหวะ และไม่ได้ถูกแก้เพียงเพราะมี queue คั่นกลาง

ส่วนอุปกรณ์ร่วมอย่างบัส I2C หรือหน้าจอ อาจเลือกให้มีงานเจ้าของเพียงตัวเดียวแล้วรับคำขอจากงานอื่น หรือใช้กลไกล็อกตามลำดับที่กำหนด วิธีใดเหมาะต้องดูขนาดงาน แต่ควรหลีกเลี่ยงการถือ lock ระหว่างรอเครือข่ายยาวๆ โดยไม่มีเหตุผลเกี่ยวข้อง
queue ไม่ได้ทำให้ผู้รับเร็วขึ้นเอง
queue ช่วยแยกจังหวะส่งกับรับได้ แต่ถ้าผู้ผลิตข้อมูลเร็วกว่าเวลาที่ผู้รับทำต่อเนื่องนานพอ พื้นที่ก็ยังเต็ม จึงต้องมีขอบเขตและนโยบายว่าเมื่อเต็มแล้วจะรอ ทิ้งรายการใด หรือรายงานความผิดพลาดอย่างไร
งานหน้าจอที่ใช้เพียงสถานะล่าสุดอาจไม่ต้องวาดทุกค่าที่ผ่านเข้ามา ขณะที่งานเก็บเหตุการณ์อาจต้องรักษาครบทุกรายการ การใช้ queue รูปแบบเดียวกับทุกงานโดยไม่ดูความหมายจึงอาจทำให้หน้าจอตามข้อมูลเก่าจนหน่วง หรือทำให้เหตุการณ์สำคัญถูกทับโดยไม่รู้ตัว
ควรเก็บจำนวนรายการค้างและเวลาที่แต่ละงานใช้ระหว่างทดสอบ หากเห็นคิวโตตลอด ก็เป็นข้อมูลว่าระบบยังรับภาระต่อเนื่องไม่ไหว การเพิ่มความยาวคิวเพียงอย่างเดียวอาจช่วยได้ชั่วคราว แต่ยังไม่เปลี่ยนอัตราที่ผู้รับทำงานได้
stack กับการคืนเวลาก็เป็นต้นทุนของแต่ละ task
แต่ละ task ต้องมี stack และส่วนจัดการของมัน จึงควรดูการใช้จริงกับขอบเขตที่เหลือ ไม่กำหนดขนาดเท่ากันทุกงานโดยไม่ได้อ่านรายละเอียด framework โดยเฉพาะหน่วยของ API ที่อาจต่างกันระหว่าง ESP-IDF กับตัวอย่าง FreeRTOS ทั่วไป
งานตามรอบควรใช้วิธีรอที่ scheduler รองรับ ไม่วนอ่านเวลาอย่างเดียวตลอด และต้องดูด้วยว่าหากทำงานเกินรอบจะจัดการอย่างไร การแบ่ง task ไม่ได้ทำให้ deadline ที่เกินไปแล้วกลับมาตรงเอง หากมีงานค้างสะสมก็ต้องรู้ว่าจะข้ามหรือจัดลำดับใหม่ตามหน้าที่ใด
เมื่อมีการหยุดหรือเริ่มงานใหม่ ควรจัดการคิว ตัวชี้ และทรัพยากรที่เป็นเจ้าของให้ครบด้วย ไม่ให้ task ที่เลิกไปทิ้ง lock หรือหน่วยความจำที่อีกงานยังอ้างอยู่ เรื่องวงจรชีวิตนี้เป็นส่วนหนึ่งของการแบ่งงาน ไม่ใช่งานเก็บรายละเอียดหลังเขียนเสร็จเท่านั้น
ก่อนรวมงานทั้งหมด ลองให้ผู้ผลิตข้อมูลส่งเร็วกว่าปกติและให้ผู้รับช้าลงในชุดทดสอบ แล้วดูว่าคิวเต็มตามนโยบายที่กำหนดจริงหรือไม่ การตรวจจุดนี้ช่วยให้เรารู้พฤติกรรมตอนภาระสูง โดยยังไม่ต้องรอให้ปัญหาเกิดกับอุปกรณ์ที่ติดตั้งไปแล้ว
สรุป
FreeRTOS task ช่วยจัดงานที่มีจังหวะและการรอต่างกัน แต่ต้องออกแบบเจ้าของข้อมูล การส่งต่อ และขอบเขตทรัพยากรไปด้วยกัน เมื่อวัดภาระกับคิวที่เกิดจริง เราจะรู้ว่าควรแบ่งงานตรงไหนและแก้อัตราประมวลผลส่วนใด การมีหลาย task จึงจะช่วยให้ระบบตามได้ง่ายขึ้น โดยไม่เพิ่มจุดแย่งกันมากกว่าประโยชน์ที่ได้รับครับ


