ทำความรู้จักกับหัวใจของ Node.js: Single-threaded คืออะไร
เวลาเราพูดถึงการเขียนโปรแกรมฝั่ง Backend (ระบบหลังบ้าน) ด้วย Node.js หลายคนมักได้ยินคำว่ามันเป็นแบบ Single-threaded (ทำงานทีละหนึ่งอย่างในหนึ่งช่วงเวลา) ฟังดูเหมือนจะช้าใช่ไหมครับ แต่ความจริงคือมันออกแบบมาเพื่อรับมือกับงานจำนวนมากได้อย่างยอดเยี่ยม โดยไม่ต้องเปิดหลายหน้าต่างงานให้เปลืองทรัพยากรเครื่อง
ลองจินตนาการถึงร้านกาแฟที่มีพนักงานชงกาแฟเพียงคนเดียว ถ้าพนักงานคนนี้มัวแต่ยืนรอเครื่องชงกาแฟทำงานเสร็จแล้วค่อยรับออเดอร์ถัดไป ร้านคงเจ๊งแน่นอน Node.js ทำงานเหมือนพนักงานที่ฉลาดกว่านั้น คือเมื่อกดปุ่มชงกาแฟแล้ว เขาจะหันไปรับออเดอร์จากลูกค้าคนถัดไปทันทีโดยไม่ยืนรอ ทำให้ร้านดูเหมือนมีคนทำงานอยู่ตลอดเวลา
การเข้าใจเรื่องนี้สำคัญมากสำหรับมือใหม่ เพราะถ้าคุณเขียนโค้ดโดยไม่เข้าใจธรรมชาติของมัน คุณอาจจะเขียนโปรแกรมที่ไป "บล็อก" (ขัดขวาง) การทำงานของคนอื่นโดยไม่รู้ตัว การเป็นโปรแกรมเมอร์ที่เก่งต้องเริ่มจากการเข้าใจว่า สภาพแวดล้อมในการรันโค้ด (เครื่องมือที่ใช้ประมวลผลคำสั่ง) ของเราทำงานอย่างไร เพื่อให้เราเขียนโปรแกรมที่ลื่นไหลและตอบสนองได้รวดเร็ว
// ตัวอย่างการทำงานแบบ Synchronous (ทำตามลำดับขั้นตอน)
console.log("เริ่มรับออเดอร์");
// สมมติว่านี่คือการรอให้กาแฟชงเสร็จ ซึ่งใช้เวลานาน
console.log("กำลังชงกาแฟ...");
console.log("รับออเดอร์ถัดไป");
จากตัวอย่างข้างต้น console.log จะถูกพิมพ์ออกมาตามลำดับบรรทัดที่ 1, 2 และ 3 หากบรรทัดที่ 2 เป็นงานที่ใช้เวลานานมาก บรรทัดที่ 3 จะไม่มีทางถูกเรียกใช้จนกว่าบรรทัดที่ 2 จะเสร็จสิ้น นี่คือปัญหาของโปรแกรมแบบดั้งเดิมที่มือใหม่มักเจอ
ผลลัพธ์ที่ควรเห็นใน Terminal (โปรแกรมสั่งงานผ่านตัวอักษร):
เริ่มรับออเดอร์
กำลังชงกาแฟ...
รับออเดอร์ถัดไป
Call Stack: สมุดจดลำดับงานของโปรแกรม
Call Stack (กองซ้อนที่เก็บตำแหน่งงานที่ต้องทำ) เปรียบเสมือนสมุดจดบันทึกของโปรแกรมเมอร์ เมื่อมีการเรียกใช้งานฟังก์ชัน (คำสั่งที่รวมกลุ่มการทำงาน) คอมพิวเตอร์จะจดบันทึกไว้ที่ด้านบนสุดของกองนี้เสมอ และเมื่อทำงานเสร็จก็จะลบงานนั้นออกไป เพื่อกลับไปทำงานก่อนหน้า
หากคุณเรียกฟังก์ชันซ้อนกันหลายชั้น Call Stack จะค่อยๆ สูงขึ้นเรื่อยๆ จนกว่างานชิ้นในสุดจะเสร็จ หากงานใดงานหนึ่งค้างอยู่ข้างในนานเกินไป งานอื่นๆ ที่ต่อคิวอยู่ก็จะถูกขวางไว้ทั้งหมด นี่คือเหตุผลว่าทำไมเราถึงต้องระวังเรื่องการใช้คำสั่งที่ใช้เวลานานภายในฟังก์ชันหลัก
การฝึกเขียนโค้ดให้ดี คุณควรหมั่นเช็คว่าฟังก์ชันที่เราเขียนมีความซับซ้อนเกินไปหรือไม่ การมี Call Stack ที่สะอาดและสั้นจะช่วยให้โปรแกรมของคุณทำงานได้เร็วขึ้น และช่วยให้คุณไล่หาจุดผิด (บั๊ก) ได้ง่ายขึ้นมากเมื่อเกิดปัญหาในอนาคต
function a() {
console.log("ทำงานที่ A");
}
function b() {
a();
console.log("ทำงานที่ B");
}
b(); // เริ่มเรียกฟังก์ชัน b
ในโค้ดนี้ b() จะถูกนำเข้า Call Stack ก่อน จากนั้นเรียก a() เข้าไปวางทับด้านบน เมื่อ a() ทำงานเสร็จก็จะถูกหยิบออก แล้ว b() ถึงจะทำงานต่อในบรรทัดถัดไป
ผลลัพธ์ที่ควรเห็นใน Terminal:
ทำงานที่ A
ทำงานที่ B
Event Loop: หัวใจหลักที่คอยเชื่อมทุกอย่างเข้าด้วยกัน
Event Loop (วงจรตรวจสอบงาน) คือตัวละครลับที่คอยเฝ้าดู Call Stack และ Event Queue (แถวคอยงานที่รอการประมวลผล) อยู่ตลอดเวลา หน้าที่ของมันง่ายมากคือถ้า Call Stack ว่างลงเมื่อไหร่ มันจะรีบหยิบงานจากแถวคอยมาใส่ให้ทำทันที
ถ้าไม่มี Event Loop โปรแกรมของเราคงเป็นเหมือนหุ่นยนต์ที่ทำตามสั่งแล้วหยุดนิ่ง แต่ด้วยกลไกนี้ Node.js จึงสามารถทำงานแบบ Asynchronous (การทำงานที่ไม่ต้องรอให้อีกงานเสร็จก่อน) ได้อย่างสมบูรณ์แบบ ทำให้ระบบหลังบ้านสามารถรองรับผู้ใช้งานพร้อมกันได้หลายคนโดยไม่ล่ม
ในฐานะมือใหม่ คุณอาจยังไม่ต้องเขียน Event Loop เอง แต่การเข้าใจว่ามันทำงานอย่างไรจะเปลี่ยนวิธีคิดของคุณไปตลอดกาล คุณจะเริ่มมองหาจุดที่สามารถทำแบบ Asynchronous ได้แทนที่จะเขียนโค้ดแบบรอคิวกันยาวเหยียด ซึ่งเป็นทักษะสำคัญที่บริษัทไอทีใช้คัดเลือกโปรแกรมเมอร์เข้าทำงาน
console.log("เริ่ม");
// setTimeout เป็นฟังก์ชันที่จำลองงานใช้เวลา
setTimeout(() => {
console.log("งานที่ใช้เวลาเสร็จแล้ว");
}, 0);
console.log("จบ");
แม้จะตั้งเวลาเป็น 0 แต่ setTimeout จะถูกส่งไปที่ Web APIs หรือระบบจัดการของ Node.js ก่อน แล้วค่อยเข้าแถวคอย ทำให้คำสั่ง console.log("จบ") ทำงานก่อนเสมอ
ผลลัพธ์ที่ควรเห็นใน Terminal:
เริ่ม
จบ
งานที่ใช้เวลาเสร็จแล้ว
Event Queue และการจัดการงานที่ค้างอยู่
Event Queue (แถวคอยที่รอให้งานว่าง) คือสถานที่พักของงานที่ยังทำไม่ได้ในขณะที่ Call Stack กำลังยุ่งอยู่ เมื่อฟังก์ชันอย่าง setTimeout หรือการอ่านไฟล์จากฐานข้อมูลทำงานเสร็จ งานเหล่านั้นจะถูกส่งมาต่อแถวรอที่นี่ เพื่อรอเวลาที่เหมาะสมในการประมวลผล
ความน่าสนใจคือ Event Queue ไม่ใช่แถวเดียวจบ แต่มีการแบ่งลำดับความสำคัญของงานด้วย เพื่อให้งานที่สำคัญจริงๆ ได้ทำก่อนงานทั่วไป นี่คือเหตุผลที่บางครั้งโปรแกรมอาจทำงานเร็วกว่าที่เราคาดไว้ หากเราเข้าใจลำดับการจัดการของมันอย่างถูกต้อง
มือใหม่หลายคนมักสับสนว่าทำไมโค้ดถึงไม่ทำงานตามลำดับที่เขียนไว้ การทำความเข้าใจว่า Event Queue ทำงานอย่างไรจะช่วยแก้ปัญหานี้ได้ คุณจะเริ่มเห็นภาพว่าโค้ดบรรทัดไหนบ้างที่ถูกโยนออกไปทำงานข้างนอก และบรรทัดไหนที่ทำงานทันทีใน Call Stack
// การจำลองการทำงานที่ต้องรอข้อมูลจากภายนอก
setTimeout(() => console.log("งาน A"), 100);
setTimeout(() => console.log("งาน B"), 0);
console.log("งานหลัก");
แม้ งาน A จะถูกเขียนก่อน แต่ งาน B จะเข้าคิวและเสร็จก่อนเพราะเวลารอน้อยกว่า ส่วน งานหลัก จะถูกประมวลผลทันทีเพราะอยู่ใน Call Stack
ผลลัพธ์ที่ควรเห็นใน Terminal:
งานหลัก
งาน B
งาน A
Microtask และ Macrotask: ลำดับความสำคัญที่ต้องรู้
ในระบบของ Node.js ยังมีสิ่งที่เรียกว่า Microtask Queue (แถวคอยงานพิเศษที่มีลำดับสูงสุด) ซึ่งสำคัญกว่า Macrotask Queue (แถวคอยงานทั่วไป) โดย Microtask มักมาจาก Promise (งานที่สัญญาว่าจะเสร็จในอนาคต) ซึ่งจะถูกรันให้เสร็จก่อนงานอื่นๆ เสมอ
การแยกแยะระหว่าง Microtask และ Macrotask คือกุญแจสำคัญในการแก้บั๊กที่ซับซ้อน หากคุณเขียนโปรแกรมที่ต้องจัดการข้อมูลจำนวนมาก การจัดลำดับให้งานที่สำคัญที่สุดอยู่ใน Microtask จะทำให้แอปพลิเคชันของคุณตอบสนองได้รวดเร็วและแม่นยำกว่าการปล่อยให้ทุกอย่างปนกันอยู่ในแถวคอยเดียว
จำไว้เสมอว่า Event Loop จะกวาดล้าง Microtask ให้หมดเกลี้ยงก่อนที่จะหันไปมอง Macrotask เพียงหนึ่งงาน นี่เป็นกฎเหล็กที่โปรแกรมเมอร์มืออาชีพทุกคนต้องแม่นยำ เพื่อให้แน่ใจว่าลำดับการทำงานของระบบเป็นไปตามที่เราออกแบบไว้
console.log("เริ่มต้น");
setTimeout(() => console.log("Macrotask"), 0);
Promise.resolve().then(() => console.log("Microtask"));
console.log("สิ้นสุด");
แม้ setTimeout จะถูกเรียกก่อน Promise แต่ Microtask อย่าง Promise จะได้สิทธิพิเศษในการทำงานก่อน Macrotask เสมอเมื่อ Call Stack ว่าง
ผลลัพธ์ที่ควรเห็นใน Terminal:
เริ่มต้น
สิ้นสุด
Microtask
Macrotask
สรุป: การนำความเข้าใจเรื่อง Event Loop ไปใช้จริง
การเป็นโปรแกรมเมอร์ไม่ได้หมายความว่าต้องจำทฤษฎีได้ทั้งหมด แต่หมายถึงการเข้าใจว่าเครื่องมือที่เราใช้ทำงานอย่างไร เพื่อให้เราเขียนโค้ดที่ มีประสิทธิภาพ (ทำงานได้ดีโดยใช้ทรัพยากรน้อย) และไม่สร้างปัญหาให้ระบบในระยะยาว สถาปัตยกรรม Event Loop คือหัวใจที่ทำให้ Node.js ครองใจนักพัฒนาทั่วโลก
เมื่อคุณเริ่มสร้างโปรเจกต์จริง เช่น การทำ API เชื่อมต่อฐานข้อมูล ให้จำไว้ว่างานทุกอย่างที่ต้องรอ (เช่น การดึงข้อมูลผู้ใช้) ควรเขียนในรูปแบบ Asynchronous เสมอ เพื่อไม่ให้ Call Stack ของคุณค้างจนผู้ใช้งานคนอื่นต้องรอคอย การฝึกฝนเรื่องนี้จะทำให้คุณก้าวข้ามจากมือใหม่ไปสู่ระดับที่ทำงานในทีมจริงได้อย่างมั่นใจ
จงหมั่นทดลองและสังเกตลำดับการรันโค้ดด้วย console.log อยู่เสมอ แล้วคุณจะเริ่มเห็นภาพการทำงานของระบบที่ซับซ้อนนี้ชัดเจนขึ้นเอง หากวันนี้ยังไม่เข้าใจทั้งหมดก็ไม่เป็นไรครับ เพราะการเป็นโปรแกรมเมอร์คือการเรียนรู้ไปพร้อมกับการลงมือทำจริงทุกวัน