2020-10-10
สวัสดีครับ ไม่ได้เขียน blog มานานเป็นเดือนละ เนื่องจากผมค่อนข้างยุ่งกับการเปลี่ยนงานใหม่และงานก็ค่อนข้างถาโถมเข้ามาแบบเยอะเลยทีเดียว (555) แต่ก็ยังพอมีเวลาไปศึกษา node.js
เพิ่มเติมอยู่บ้าง โดยเฉพาะการทำงานภายในของตัว node.js
เอง ซึ่ง blog นี้ก็จะนำเสนอเกี่ยวกับ Event Loop ซึ่งมีบทบาทสำคัญเป็นอย่างมากในตัว node.js
core หวังว่าจะเป็นประโยชน์ต่อคนที่กำลังศึกษา node.js
อยู่นะครับ code และเนื้อหาหลักๆ ผมจะอ้างอิงจาก Node JS: Advanced Concepts ของ Stephen Grider ครับผม
Node.js คือ runtime environment เพื่อดำเนินการ run Javascript ฝั่ง server side เราทราบกันดีอยู่แล้วว่า Javascript ทำงานบน Single-Threaded ดังนั้น Event Loop คือสิ่งที่คอยจัดการ asynchronous non-blocking I/O operations ใน node นั่นเอง เปรียบเสมือนเป็นโครงสร้างที่ควบคุมและตัดสินใจว่าจะทำ task ใดในช่วงเวลาหนึ่งบน thread นั้นๆ ซึ่งการทำงานของ Event Loop จะทำงานอยู่บน thread เดียวโดยมีวัฏจักรการทำงานตาม diagram รูปล่าง
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
Ref: The Node.js Event Loop, Timers, and process.nextTick()
สมมติว่าเราเขียน code ในไฟล์ index.js
แล้วเราจะใช้คำสั่ง run node index.js
แต่ละช่วง Event Loop จะมีการทำงานดังต่อไปนี้
setTimeout()
หรือ setInterval()
setImmediate()
callbackssocket.on('close', ...)
ทีนี้อาจจะยังมองภาพไม่ออกว่าแต่ละช่วงนั้น Event Loop มันทำงานของมันยังไงกันแน่ ต่อไปผมจะเอาแต่ละช่วงมาเขียนเป็น psuedo code ดูนะครับ เพื่อให้เข้าใจมากขึ้น
//----- Totally FAKE code -----//
// สมมติว่า run node index.js
// Tasks ต่างๆ ที่ Event Loop ต้องทำการตรวจสอบ
// และตัดสินใจว่าจะดำเนินการอย่างไรต่อไป
const pendingTimers = [];
const pendingOSTasks = [];
const pendingOperations = [];
// shouldContinue() คือตัวแทนการทำงานของ Event Loop
// ซึ่ง function นี้จะคืนค่า truthy หรือ falsy
function shouldContinue() {
return (
pendingTimers.length || pendingOSTasks.length || pendingOperations.length
);
}
// เข้าสู่ Event Loop
while (shouldContinue()) {}
// exit back to terminal
รายละเอียดการทำงานในแต่ละช่วงของ Event Loop มีดังนี้
pendingTimers
หรือไม่ ซึ่งก็คือ setTimeout()
และ setInterval()
ถ้ามี ก็จะทำการ execute callbacks function ที่อยู่ใน pendingTimers
เหล่านั้นpendingOSTasks
และ pendingOperations
อยู่หรือไม่ เช่น การเรียก httpServer.listen(PORT)
หรือการเรียกใช้ file system module fs
pendingOSTasks
เสร็จpendingOperations
เสร็จpendingTimers
อีกครั้งและเรียก setImmediate
functionclose event
เช่น socket.on('close', ...)
หรือ readStream.on('close', ...)
ถ้ามี pendingTask
ใดใดสักอันหนึ่งที่มี task อยู่ข้างใน (length > 0) shouldContinue()
ก็จะคืนค่าเป็น true
Event Loop ก็จะดำเนินวัฏจักรของมันไปเรื่อยๆ (while (true) {}
) จนกว่าทุก ๆ pendingTask
หมดไป shouldContinue()
ก็จะคืนค่าเป็น false
และออกมาจาก loop นั่นเอง
มาถึงตรงนี้ อยากจะตั้งคำถามกับคุณผู้อ่านสักหน่อยว่า Node.js เป็น single-thread หรือไม่?
คำตอบคือ...
ทั้งใช่ และไม่ใช่...
เริ่มงงกันแล้วใช่มั้ยครับ 555
ผมจะแบ่งเป็น 2 ประเด็นหลักๆ ละกันนะครับ
fs, crypto, zlib
- ไม่ใช่ single-threaded ครับบางคนอาจจะสงสัย ว่าอ้าว javascript เป็น single-threaded นี่ node เองก็ใช้ javascript แล้วทำไมไม่ใช่ single-thread ทั้งหมดล่ะ
ลองมาดูตัวอย่างกันครับ
// thread.js
const { pbkdf2 } = require('crypto');
const start = Date.now();
pbkdf2('pwd', 'salt', 100000, 512, 'sha512', () =>
console.log(`1: Done in ${Date.now() - start}ms`)
);
pdkdf2
เป็น function ที่ใช้ทำการเข้ารหัส ในตัวอย่างจะใช้ sha512
เป็น secure hash algorithm ทีนี้ลอง run node thread.js
ดูครับ ก็จะได้ผลลัพธ์ประมาณนี้
ทีนี้ลองเพิ่มไปอีก 3 ชุดคำสั่ง
const { pbkdf2 } = require('crypto');
const start = Date.now();
pbkdf2('pwd', 'salt', 100000, 512, 'sha512', () =>
console.log(`1: Done in ${Date.now() - start}ms`)
);
pbkdf2('pwd', 'salt', 100000, 512, 'sha512', () =>
console.log(`2: Done in ${Date.now() - start}ms`)
);
pbkdf2('pwd', 'salt', 100000, 512, 'sha512', () =>
console.log(`3: Done in ${Date.now() - start}ms`)
);
pbkdf2('pwd', 'salt', 100000, 512, 'sha512', () =>
console.log(`4: Done in ${Date.now() - start}ms`)
);
แล้ว run ดูอีกครั้งครับจะได้
อ้าววววว ทำไมเหมือนกับว่าเกิดขึ้นพร้อมกันเลยล่ะ? ถ้า node เป็น single-threaded จริง มันต้องทำตามลำดับ 1 --> 2 --> 3 --> 4 ใช้มั้ยครับ ดูจากรูปผม run หลายๆ ครั้งให้ดูเลย เพื่อให้เห็นชัดเจนว่ามันเกิดขึ้น ๆ พร้อมๆ กัน
สาเหตุที่เป็นแบบนี้ ก็เพราะว่า standard module บางตัว (เช่น crypto) ที่มีการใช้ cpu ประมวลผลสูง node จะไม่ได้ run แค่ single-threaded แต่จะอาศัยตัวช่วยอย่าง uv thread pool โดยที่ค่าเริ่มต้นของ uv_thread_pool
ก็คือ 4 ถ้าเราอยากให้มัน run แค่ thread เดียว ก็สามารถทำได้แบบนี้ครับ
// กำหนด UV_THREADPOOL_SIZE แบบนี้
// ใส่ไว้ข้างบนสุดของ code เมื่อกี้
process.env.UV_THREADPOOL_SIZE = 1;
แล้วทีนี้ลอง run ดูอีกครั้งครับ ผลที่ได้คือ
ถ้าลองไปอ่านที่ doc ของ libuv จะพบว่าเราสามารถกำหนด UV_THREADPOOL_SIZE
ได้ถึง 1024 ลองไปเล่นๆ กันดูได้ครับ สรุปคือ node จะใช้ Event Loop บน main thread และจะมี threadpool จาก libuv เอาไว้คอยจัดการพวก cpu intensive task
ต่อไปก็มาทำความเข้าใจเกี่ยวกับ os operations
หรือ pendingOSTasks
ที่ผมเขียนไว้ใน psuedo code ตัวอย่างนะครับ ซึ่งเป็นอีกตัวที่น่าสนใจเช่นกัน ลองดู code ตัวอย่างกันครับ
// async.js
const https = require('https');
const start = Date.now();
function doRequest() {
https
.request('https://www.google.com', res => {
res.on('data', () => {});
res.on('end', () => {
console.log(Date.now() - start);
});
})
.end();
}
doRequest();
แล้ว run node async.js
จะได้เป็น
ทีนี้ลองเพิ่มการเรียก function doRequest()
ให้เป็นสัก 7 ครั้ง
doRequest();
doRequest();
doRequest();
doRequest();
doRequest();
doRequest();
อ้าวววว เรียกไปตั้ง 6 ครั้ง แต่ก็ run พร้อมๆ กันแหะ! ยังไงเนี่ยยยย
คำตอบก็คือเมื่อมีการทำ network request แบบ code ข้างบน node จะไม่ได้ใช้ UV_THREADPOOL
แต่จะไปเรียกใช้ asynchronous interface ซึ่งแต่ละระบบปฏิบัติการมีให้อยู่แล้วแทน ถ้าเป็น linux ก็จะเป็น AIO เป็นต้น
หวังว่าจะช่วยคลายข้อสงสัยเกี่ยวกับการทำงานของ Event Loop ไม่มากก็น้อยนะครับ ถ้ามีเวลา ผมจะมาเขียนเกี่ยวกับสิ่งที่น่าสนใจของ Node.js อีกเรื่อยๆ นะครับ
Happy Coding :)