หากคุณต้องถามคำถามนี้แสดงว่าคุณอาจไม่คุ้นเคยกับสิ่งที่เว็บแอปพลิเคชัน / บริการส่วนใหญ่ทำ คุณอาจกำลังคิดว่าซอฟต์แวร์ทั้งหมดทำสิ่งนี้:
user do an action
│
v
application start processing action
└──> loop ...
└──> busy processing
end loop
└──> send result to user
อย่างไรก็ตามนี่ไม่ใช่วิธีที่เว็บแอปพลิเคชันหรือแอปพลิเคชันใด ๆ ที่มีฐานข้อมูลเป็นแบ็คเอนด์ใช้งานได้ เว็บแอปทำสิ่งนี้:
user do an action
│
v
application start processing action
└──> make database request
└──> do nothing until request completes
request complete
└──> send result to user
ในสถานการณ์สมมตินี้ซอฟต์แวร์ใช้เวลาส่วนใหญ่ในการทำงานโดยใช้เวลา CPU 0% ที่รอให้ฐานข้อมูลส่งคืน
แอพเครือข่ายแบบมัลติเธรด:
แอพเครือข่ายแบบมัลติเธรดจัดการกับปริมาณงานด้านบนดังนี้:
request ──> spawn thread
└──> wait for database request
└──> answer request
request ──> spawn thread
└──> wait for database request
└──> answer request
request ──> spawn thread
└──> wait for database request
└──> answer request
ดังนั้นเธรดใช้เวลาส่วนใหญ่ในการใช้ CPU 0% ที่รอให้ฐานข้อมูลส่งคืนข้อมูล ในขณะที่ทำเช่นนั้นพวกเขาจะต้องจัดสรรหน่วยความจำที่จำเป็นสำหรับเธรดซึ่งรวมถึงสแต็คโปรแกรมที่แยกจากกันอย่างสมบูรณ์สำหรับแต่ละเธรดเป็นต้นนอกจากนี้พวกเขาจะต้องเริ่มเธรดที่ในขณะที่ไม่แพง ถูก
วนรอบเหตุการณ์แบบเธรดเดี่ยว
เนื่องจากเราใช้เวลาส่วนใหญ่ไปกับการใช้ CPU 0% ทำไมไม่ลองใช้งานโค้ดเมื่อเราไม่ได้ใช้ซีพียู? ด้วยวิธีนี้การร้องขอแต่ละครั้งจะยังคงได้รับเวลา CPU เท่ากันกับแอปพลิเคชันแบบมัลติเธรด แต่เราไม่จำเป็นต้องเริ่มเธรด ดังนั้นเราจึงทำสิ่งนี้:
request ──> make database request
request ──> make database request
request ──> make database request
database request complete ──> send response
database request complete ──> send response
database request complete ──> send response
ในทางปฏิบัติทั้งสองวิธีจะส่งคืนข้อมูลโดยมีเวลาแฝงเท่ากันเนื่องจากเป็นเวลาตอบสนองฐานข้อมูลที่ใช้ในการประมวลผล
ข้อได้เปรียบหลักที่นี่คือเราไม่จำเป็นต้องวางไข่เธรดใหม่ดังนั้นเราจึงไม่จำเป็นต้องทำ malloc จำนวนมากซึ่งจะทำให้เราช้าลง
เธรดวิเศษที่มองไม่เห็น
สิ่งลึกลับที่ดูเหมือนจะเป็นวิธีการทั้งสองวิธีข้างต้นจัดการเพื่อให้ปริมาณงานใน "ขนาน"? คำตอบคือฐานข้อมูลเป็นเธรด ดังนั้นแอพพลิเคชั่นเธรดเดี่ยวของเราจึงใช้ประโยชน์จากพฤติกรรมแบบมัลติเธรดของกระบวนการอื่น: ฐานข้อมูล
ตำแหน่งที่เธรดเดี่ยวล้มเหลว
แอพที่มีเธรดเดียวล้มเหลวใหญ่ถ้าคุณต้องการคำนวณ CPU จำนวนมากก่อนส่งคืนข้อมูล ตอนนี้ฉันไม่ได้หมายถึงการประมวลผลลูปสำหรับฐานข้อมูล ส่วนใหญ่ยังคงเป็น O (n) สิ่งที่ฉันหมายถึงคือการแปลงฟูริเยร์ (เช่นการเข้ารหัส MP3), การติดตามเรย์ (การเรนเดอร์ 3D) เป็นต้น
ข้อผิดพลาดอีกประการหนึ่งของแอพพลิเคชั่นที่มีเธรดเดียวคือมันจะใช้ CPU แกนเดียว ดังนั้นหากคุณมีเซิร์ฟเวอร์ quad-core (ไม่ใช่เรื่องแปลกในปัจจุบัน) คุณไม่ได้ใช้ 3 คอร์อื่น
ที่วิธีการแบบมัลติเธรดล้มเหลว
แอพมัลติเธรดล้มเหลวใหญ่ถ้าคุณต้องการจัดสรร RAM จำนวนมากต่อเธรด ก่อนอื่นการใช้ RAM นั้นหมายความว่าคุณไม่สามารถจัดการคำขอได้มากเท่ากับแอปที่มีเธรดเดียว แย่ลง malloc ช้า การจัดสรรล็อตและวัตถุจำนวนมาก (ซึ่งเป็นเรื่องปกติสำหรับเฟรมเวิร์กเว็บสมัยใหม่) หมายความว่าเราอาจจะจบลงด้วยการช้ากว่าแอพที่มีเธรดเดียว นี่คือที่ node.js มักจะชนะ
กรณีการใช้งานอย่างหนึ่งที่ทำให้การทำมัลติเธรดแย่ลงคือเมื่อคุณต้องการเรียกใช้ภาษาสคริปต์อื่นในเธรดของคุณ ก่อนอื่นคุณจะต้อง malloc รันไทม์ทั้งหมดสำหรับภาษานั้นจากนั้นคุณต้อง malloc ตัวแปรที่ใช้โดยสคริปต์ของคุณ
ดังนั้นหากคุณเขียนแอปเครือข่ายใน C หรือ go หรือ java ค่าใช้จ่ายในการทำเกลียวมักจะไม่เลวร้ายเกินไป หากคุณกำลังเขียนเว็บเซิร์ฟเวอร์ C เพื่อให้บริการ PHP หรือ Ruby คุณสามารถเขียนเซิร์ฟเวอร์ได้เร็วขึ้นใน javascript หรือ Ruby หรือ Python
วิธีไฮบริด
เว็บเซิร์ฟเวอร์บางตัวใช้วิธีไฮบริด ตัวอย่างเช่น Nginx และ Apache2 ใช้รหัสการประมวลผลเครือข่ายเป็นกลุ่มเธรดของลูปเหตุการณ์ แต่ละเธรดรันลูปเหตุการณ์พร้อมกันในการประมวลผลคำร้องขอแบบเธรดเดียว แต่คำร้องขอมีความสมดุลโหลดระหว่างหลายเธรด
สถาปัตยกรรมแบบเธรดเดียวบางตัวยังใช้วิธีไฮบริด แทนที่จะเปิดใช้หลายเธรดจากกระบวนการเดียวคุณสามารถเปิดใช้งานหลายแอปพลิเคชัน - ตัวอย่างเช่นเซิร์ฟเวอร์ 4 node.js บนเครื่อง quad-core จากนั้นคุณใช้ตัวโหลดบาลานซ์เพื่อกระจายเวิร์กโหลดระหว่างกระบวนการ
ในทางปฏิบัติทั้งสองวิธีจะมีภาพสะท้อนเหมือนกันในทางเทคนิคของกันและกัน