ตอนนี้ Node.js กลายเป็นสมาชิกของกล่องเครื่องมือสำหรับการสร้างบริการแอปพลิเคชันเครือข่ายที่ทำงานพร้อมกันสูง เหตุใด Node.js จึงกลายเป็นที่รักของสาธารณชน บทความนี้จะเริ่มต้นด้วยแนวคิดพื้นฐานของกระบวนการ เธรด โครูทีน และโมเดล I/O และให้ข้อมูลเบื้องต้นที่ครอบคลุมเกี่ยวกับ Node.js และโมเดลการทำงานพร้อมกัน
โดยทั่วไปเราเรียกอินสแตนซ์ที่กำลังทำงานอยู่ของโปรแกรมว่ากระบวนการ เป็นหน่วยพื้นฐานสำหรับการจัดสรรทรัพยากรและกำหนดเวลาโดยระบบปฏิบัติการ โดยทั่วไปจะประกอบด้วยส่วนต่างๆ ต่อไปนี้:
进程表
แต่ละกระบวนการครอบครองรายการ进程表项
(หรือที่เรียกว่า进程控制块
) รายการนี้ประกอบด้วยสถานะกระบวนการที่สำคัญ เช่น ตัวนับโปรแกรม ตัวชี้สแต็ก การจัดสรรหน่วยความจำ สถานะของไฟล์ที่เปิด และข้อมูลกำหนดการ ข้อมูลเพื่อให้แน่ใจว่าหลังจากกระบวนการถูกระงับระบบปฏิบัติการสามารถฟื้นฟูกระบวนการได้อย่างถูกต้องกระบวนการมีลักษณะดังต่อไปนี้:
ควรสังเกตว่าหากโปรแกรมถูกรันสองครั้ง แม้ว่าระบบปฏิบัติการจะสามารถเปิดใช้งานให้แชร์โค้ดได้ (เช่น มีโค้ดเพียงสำเนาเดียวเท่านั้นที่อยู่ในหน่วยความจำ) ก็ไม่สามารถเปลี่ยนแปลงได้ว่าทั้งสองอินสแตนซ์ของโปรแกรมที่รันอยู่นั้นแตกต่างกันสองรายการ ประมวลผลข้อเท็จจริง
ในระหว่างการดำเนินการกระบวนการ เนื่องจากสาเหตุหลายประการ เช่น การหยุดชะงักและการตั้งเวลา CPU กระบวนการจะสลับระหว่างสถานะต่อไปนี้:
จากแผนภาพการสลับสถานะกระบวนการด้านบน เราจะเห็นว่ากระบวนการสามารถเปลี่ยนจากสถานะกำลังทำงานเป็นสถานะพร้อมและสถานะถูกบล็อกได้ แต่มีเพียงสถานะพร้อมเท่านั้นที่สามารถเปลี่ยนเป็นสถานะกำลังทำงานได้โดยตรง เนื่องจาก:
บางครั้งเราจำเป็นต้องใช้เธรดเพื่อแก้ไขปัญหาต่อไปนี้:
เกี่ยวกับเธรด เราจำเป็นต้องทราบประเด็นต่อไปนี้:
ตอนนี้เราเข้าใจคุณลักษณะพื้นฐานของเธรดแล้ว เรามาพูดถึงประเภทเธรดทั่วไปหลายประเภทกันดีกว่า
สถานะเคอร์เนลเป็นเธรดที่รองรับโดยตรงโดยระบบปฏิบัติการ มีดังต่อไปนี้:
โหมดผู้ใช้เป็นเธรดที่สร้างขึ้นอย่างสมบูรณ์ในพื้นที่ผู้ใช้ ดังนี้:
กระบวนการน้ำหนักเบา (LWP) เป็นเธรดผู้ใช้ที่สร้างขึ้นและรองรับโดยเคอร์เนล คุณสมบัติหลักมีดังนี้:
พื้นที่ผู้ใช้สามารถใช้เธรดเคอร์เนลผ่านกระบวนการน้ำหนักเบาเท่านั้น (LWP) เชื่อมโยงระหว่างเธรดโหมดผู้ใช้และเธรดเคอร์เนล ดังนั้น เฉพาะการสนับสนุนเธรดเคอร์เนลเท่านั้นที่จะมีกระบวนการแบบไลต์เวท (LWP)
การดำเนินการส่วนใหญ่ของกระบวนการแบบไลต์เวท (LWP) ต้องการพื้นที่โหมดผู้ใช้เพื่อเริ่มต้นการเรียกระบบ มีราคาค่อนข้างแพง (ต้องสลับระหว่างโหมดผู้ใช้และโหมดเคอร์เนล)
แต่ละกระบวนการแบบไลท์เวท (LWP) จำเป็นต้องเชื่อมโยงกับเคอร์เนลเธรดเฉพาะ ดังนั้น:
พวกเขาสามารถเข้าถึงกระบวนการของตนเองได้ พื้นที่ที่อยู่ที่ใช้ร่วมกันและทรัพยากรระบบทั้งหมด
ข้างต้น เราได้แนะนำประเภทเธรดทั่วไปโดยย่อ (เธรดสถานะเคอร์เนล เธรดสถานะผู้ใช้ กระบวนการแบบไลท์เวท) แต่ละรายการมีขอบเขตการใช้งานของตัวเอง คุณสามารถใช้งานได้อย่างอิสระตามความต้องการของคุณ การรวมกัน เช่น แบบตัวต่อตัวแบบทั่วไป แบบหลายต่อหนึ่ง แบบหลายต่อหลายแบบ และแบบอื่นๆ เนื่องจากข้อจำกัดด้านพื้นที่ บทความนี้จะไม่แนะนำเรื่องนี้มากนัก นักเรียนที่สนใจสามารถศึกษาได้ด้วยตนเอง
หรือที่เรียกว่า Fiber เป็นกลไกการทำงานของโปรแกรมที่สร้างขึ้นบนเธรดที่ช่วยให้นักพัฒนาสามารถจัดการกำหนดเวลาการดำเนินการ การบำรุงรักษาสถานะ และพฤติกรรมอื่น ๆ ได้ด้วยตัวเอง คุณสมบัติหลักคือ
ใน JavaScript async/await
ที่เรามักใช้คือการนำ coroutine ไปใช้ เช่นตัวอย่างต่อไปนี้:
function updateUserName(id, name) { ผู้ใช้ const = getUserById(id); user.updateName(ชื่อ); กลับเป็นจริง; - ฟังก์ชั่น async updateUserNameAsync (id, ชื่อ) { ผู้ใช้ const = รอ getUserById (id); รอ user.updateName (ชื่อ); กลับเป็นจริง; }
ในตัวอย่างข้างต้น ลำดับการดำเนินการเชิงตรรกะภายในฟังก์ชัน updateUserName
และ updateUserNameAsync
คือ:
getUserById
และกำหนดค่าส่งคืนให้กับ user
ตัวแปรupdateName
ของ user
true
ให้กับผู้เรียกความแตกต่างที่สำคัญระหว่างทั้งสองอยู่ในการควบคุมสถานะระหว่างการดำเนินการจริง:
updateUserName
จะถูกดำเนินการตามลำดับตามลำดับตรรกะที่กล่าวถึงข้างต้นupdateUserNameAsync
ก็จะถูกดำเนินการตามลำดับตามลำดับเช่นกัน ลำดับตรรกะที่กล่าวถึงข้างต้น แต่เมื่อพบ await
updateUserNameAsync
จะถูกระงับและบันทึกสถานะของโปรแกรมปัจจุบันที่ตำแหน่งที่ถูกระงับ มันจะไม่ปลุก updateUserNameAsync
อีกครั้งจนกว่าส่วนของโปรแกรมหลังจาก await
ส่งคืนและกู้คืนสถานะของโปรแกรมก่อนที่จะหยุดชั่วคราว แล้วไปต่อที่โปรแกรมถัดไปจากการวิเคราะห์ข้างต้น เราสามารถเดาได้อย่างกล้าหาญ: สิ่งที่ Coroutines จำเป็นต้องแก้ไขไม่ใช่ปัญหาการทำงานพร้อมกันของโปรแกรมที่กระบวนการและเธรดจำเป็นต้องแก้ไข แต่เป็นปัญหาที่พบเมื่อประมวลผลงานแบบอะซิงโครนัส (เช่น การทำงานของไฟล์ การร้องขอเครือข่าย ฯลฯ ); ใน ก่อน async/await
เราสามารถจัดการงานแบบอะซิงโครนัสผ่านฟังก์ชันการโทรกลับเท่านั้น ซึ่งอาจทำให้เราตกอยู่ใน回调地狱
และสร้างโค้ดที่ยุ่งเหยิงซึ่งโดยทั่วไปแล้วยากต่อการบำรุงรักษา ผ่าน coroutines เราสามารถบรรลุวัตถุประสงค์ของการซิงโครไนซ์โค้ด .
สิ่งที่ต้องจำไว้คือความสามารถหลักของโครูทีนคือสามารถระงับบางโปรแกรมและรักษาสถานะของตำแหน่งการระงับของโปรแกรม และกลับมาทำงานต่อที่ตำแหน่งที่ถูกระงับในเวลาใดเวลาหนึ่งในอนาคต และดำเนินการต่อ ดำเนินการส่วนถัดไปหลังจากโปรแกรมช่วงล่าง
การดำเนินการ I/O
ที่สมบูรณ์จะต้องผ่านขั้นตอนต่อไปนี้:
I/O
ไปยังเคอร์เนลผ่านการเรียกของระบบ;I/O
(แบ่งออกเป็น เข้าสู่ขั้นตอนการเตรียมการและขั้นตอนการดำเนินการจริง) และส่งคืนผลลัพธ์การประมวลผลไปยังเธรดผู้ใช้เราสามารถแบ่งการดำเนินการ I/O
ออกเป็นสี่ประเภทโดยประมาณ:阻塞I/O
,非阻塞I/O
,同步I/O
และ异步I/O
ก่อนที่จะพูดถึงประเภทเหล่านี้ ก่อนอื่นเราจะทำความคุ้นเคยกับสองชุดต่อไปนี้ แนวคิด (ในที่นี้ สมมติว่าบริการ A เรียกใช้บริการ B):
阻塞/非阻塞
:
阻塞调用
非阻塞调用
同步/异步
:
同步
回调
หลังจากการดำเนินการเสร็จสิ้น ผลลัพธ์จะได้รับแจ้งไปยัง A จากนั้นบริการ B จะเป็น异步
หลายๆ คนมักสับสนระหว่าง阻塞/非阻塞
กับ同步/异步
ดังนั้นจึงจำเป็นต้องให้ความสนใจเป็นพิเศษ:
阻塞/非阻塞
มีไว้สำหรับ调用者
บริการ ส่วน同步/异步
มีไว้สำหรับ被调用者
บริการหลังจากทำความเข้าใจ阻塞/非阻塞
และ同步/异步
แล้ว เรามาดู I/O 模型
ที่เฉพาะเจาะจงกัน
: หลังจากที่เธรดผู้ใช้เริ่มต้นการเรียกระบบ I/O
เธรดผู้ใช้จะ阻塞
ทันทีจนกว่าการดำเนินการ I/O
ทั้งหมดจะได้รับการประมวลผล และผลลัพธ์จะถูกส่งกลับไปยังเธรดผู้ใช้ หลังจากที่ผู้ใช้เข้าสู่เท่านั้น (เธรด) กระบวนการสามารถปล่อยสถานะ阻塞
และดำเนินการดำเนินการต่อไปได้
คุณสมบัติ:
I/O
กระบวนการของผู้ใช้ (เธรด) จึงไม่สามารถทำการดำเนินการอื่นได้I/O
หนึ่งคำขอสามารถบล็อกเธรดขาเข้า (เธรด) ได้ ดังนั้นเพื่อที่จะตอบสนองต่อคำขอ I/O
ได้ทันเวลา จึงจำเป็นต้องจัดสรรเธรดขาเข้า (เธรด) ให้กับแต่ละคำขอ ซึ่งจะทำให้เกิดทรัพยากรจำนวนมาก การใช้งาน และสำหรับการร้องขอการเชื่อมต่อที่ยาวนาน เนื่องจากทรัพยากร (เธรด) ขาเข้าไม่สามารถเผยแพร่ได้เป็นเวลานาน หากมีคำขอใหม่ในอนาคต ปัญหาคอขวดด้านประสิทธิภาพที่ร้ายแรงจะเกิดขึ้น:
I/O
ในเธรด (เธรด) หากการดำเนินการ I/O
ไม่พร้อม การเรียก I/O
จะส่งคืนข้อผิดพลาด และผู้ใช้ไม่ จำเป็นต้องเข้าสู่เธรด (เธรด) รอ แต่ใช้การโพลเพื่อตรวจสอบว่าการดำเนินการ I/O
พร้อมหรือไม่I/O
จริงจะบล็อกเธรดของผู้ใช้จนกว่าผลการดำเนินการจะถูกส่งกลับไปยัง เธรดของผู้ใช้คุณลักษณะ:
I/O
อย่างต่อเนื่อง (โดยปกติจะใช้การวนซ้ำ while
) ผู้ใช้จึงต้องใช้โมเดลนี้และใช้ทรัพยากรของ CPUI/O
จะพร้อม จำเป็นต้องป้อน (เธรด) จะไม่ถูกบล็อก เมื่อการดำเนินการ I/O
พร้อม การดำเนินการ I/O
จริงที่ตามมาจะบล็อกผู้ใช้ไม่ให้เข้าสู่เธรด (เธรดนัส) หลังจากที่กระบวนการผู้ใช้ (เธรด) เริ่มต้นการเรียกระบบ I/O
หากการเรียก I/O
ทำให้กระบวนการผู้ใช้ (เธรด) ถูกบล็อก ดังนั้นการเรียก I/O
จะเป็น同步I/O
นัส同步I/O
ฉะนั้นจะเป็น异步I/O
เกณฑ์สำหรับการตัดสินว่าการดำเนินการ I/O
同步
หรือ异步
นั้นเป็นกลไกการสื่อสารระหว่างเธรดผู้ใช้และการดำเนินการ I/O
ใน
同步
การโต้ตอบระหว่างเธรดผู้ใช้และ I/O
จะถูกซิงโครไนซ์ผ่านเคอร์เนลบัฟเฟอร์ นั่นคือเคอร์เนลจะซิงโครไนซ์ผลการดำเนินการของการดำเนินการ I/O
ไปยังบัฟเฟอร์ จากนั้นคัดลอกข้อมูลในบัฟเฟอร์ไปยังเธรดผู้ใช้ กระบวนการนี้จะบล็อกเธรดผู้ใช้จนกว่าการดำเนินการ I/O
จะเสร็จสมบูรณ์异步
สถานการณ์ การโต้ตอบระหว่างเธรดผู้ใช้ (เธรด) และ I/O
จะถูกซิงโครไนซ์โดยตรงผ่านเคอร์เนล นั่นคือ เคอร์เนลจะคัดลอกผลการดำเนินการของการดำเนินการ I/O
ไปยังเธรดผู้ใช้ (เธรด) โดยตรง ไม่บล็อกกระบวนการ (เธรด) ของผู้ใช้Node.js ใช้โมเดล I/O
แบบอะซิงโครนัสแบบเธรดเดียวที่ขับเคลื่อนด้วยเหตุการณ์ โดยส่วนตัวแล้ว ฉันคิดว่าเหตุผลในการเลือกโมเดลนี้คือ:
I/O
มาก วิธีการจัดการทรัพยากรแบบมัลติเธรดอย่างสมเหตุสมผลและมีประสิทธิภาพ ขณะเดียวกันก็ทำให้มั่นใจว่าการทำงานพร้อมกันในระดับสูงนั้นซับซ้อนกว่าการจัดการทรัพยากรแบบเธรดเดียวกล่าวโดยสรุป เพื่อจุดประสงค์ของความเรียบง่ายและมีประสิทธิภาพ Node.js ใช้โมเดล I/O
แบบอะซิงโครนัสแบบเธรดเดียวที่ขับเคลื่อนด้วยเหตุการณ์ และใช้โมเดลผ่าน EventLoop ของเธรดหลักและเธรดตัวทำงานเสริม:
ควรสังเกตว่า Node.js ไม่เหมาะสำหรับการทำงานที่ใช้ CPU มาก (เช่น ต้องใช้การคำนวณจำนวนมาก) เนื่องจากโค้ด EventLoop และ JavaScript (โค้ดงานเหตุการณ์ที่ไม่ตรงกัน) ทำงานในเธรดเดียวกัน (เช่น เธรดหลัก) และรายการใดรายการหนึ่ง หากทำงานนานเกินไป อาจทำให้เธรดหลักบล็อกได้ หากแอปพลิเคชันมีงานจำนวนมากที่ต้องใช้การดำเนินการนาน ก็จะลดปริมาณงานของเซิร์ฟเวอร์และอาจถึงขั้นนั้นด้วยซ้ำ ทำให้เซิร์ฟเวอร์ไม่ตอบสนอง
Node.js เป็นเทคโนโลยีที่นักพัฒนาส่วนหน้าต้องเผชิญทั้งในปัจจุบันและในอนาคต อย่างไรก็ตาม นักพัฒนาส่วนหน้าส่วนใหญ่มีความรู้เพียงผิวเผินเกี่ยวกับ Node.js เท่านั้น เพื่อให้ทุกคนเข้าใจรูปแบบการทำงานพร้อมกันของ Node ได้ดีขึ้น .js บทความนี้จะแนะนำกระบวนการ เธรด และโครูทีนเป็นอันดับแรก จากนั้นจะแนะนำโมเดล I/O
ต่างๆ และสุดท้ายจะให้ข้อมูลเบื้องต้นสั้นๆ เกี่ยวกับโมเดลการทำงานพร้อมกันของ Node.js แม้ว่าจะมีพื้นที่ไม่มากพอที่จะแนะนำโมเดลการทำงานพร้อมกันของ Node.js แต่ผู้เขียนเชื่อว่าไม่สามารถแยกออกจากหลักการพื้นฐานได้ การเรียนรู้พื้นฐานที่เกี่ยวข้อง จากนั้นทำความเข้าใจอย่างลึกซึ้งเกี่ยวกับการออกแบบและการใช้งาน Node.js จะได้รับผลลัพธ์สองเท่า ด้วยความพยายามครึ่งหนึ่ง
สุดท้ายนี้ หากมีข้อผิดพลาดใดๆ ในบทความนี้ ฉันหวังว่าคุณจะสามารถแก้ไขได้ ฉันหวังว่าทุกคนจะมีความสุขกับการเขียนโค้ดทุกๆ วัน