문제
웹 사이트 뒤의 대부분의 백엔드는 복잡한 계산을 수행 할 필요가 없습니다. 우리 프로그램은 디스크가 읽기 및 쓰기를 기다리거나 유선이 메시지를 전송하고 답을 되돌릴 때까지 기다리는 데 대부분의 시간을 소비합니다.
입출력 작업은 데이터 처리보다 훨씬 느릴 수 있습니다. 예를 들면 다음과 같습니다 : SSD-s의 읽기 속도는 200-730 MB / s (최소 하이 엔드)입니다. 1 킬로바이트의 데이터를 읽는 데는 1.4 마이크로 초가 걸리지 만이 시간 동안 2GHz로 클럭 된 CPU는 28 000 회의 명령 처리주기를 수행 할 수있었습니다.
네트워크 통신의 경우 더 악화 될 수 있습니다. google.com을 핑 (ping) 해보십시오.
$ ping google.com
64 bytes from 172.217.16.174: icmp_seq=0 ttl=52 time=33.017 ms
64 bytes from 172.217.16.174: icmp_seq=1 ttl=52 time=83.376 ms
64 bytes from 172.217.16.174: icmp_seq=2 ttl=52 time=26.552 ms
64 bytes from 172.217.16.174: icmp_seq=3 ttl=52 time=40.153 ms
64 bytes from 172.217.16.174: icmp_seq=4 ttl=52 time=37.291 ms
64 bytes from 172.217.16.174: icmp_seq=5 ttl=52 time=58.692 ms
64 bytes from 172.217.16.174: icmp_seq=6 ttl=52 time=45.245 ms
64 bytes from 172.217.16.174: icmp_seq=7 ttl=52 time=27.846 ms
평균 대기 시간은 약 44 밀리 초입니다. 전선에서 왕복을 기다리는 패킷을 기다리는 동안, 앞서 언급 한 프로세서는 8800 만 사이클을 수행 할 수 있습니다.
해결책
대부분의 운영 체제는 일종의 비동기 IO 인터페이스를 제공하므로 통신 결과가 필요없는 데이터 처리를 시작할 수 있으며 통신은 계속 진행됩니다.
이것은 여러 가지 방법으로 달성 될 수 있습니다. 요즘 대부분의 경우 멀티 스레딩의 가능성을 활용하여 소프트웨어 복잡성이 추가로 발생합니다. 예를 들어 Java 또는 Python으로 파일을 읽는 것은 차단 작업입니다. 네트워크 / 디스크 통신이 끝나기를 기다리는 동안 프로그램은 다른 작업을 수행 할 수 없습니다. 적어도 Java에서는 할 수있는 일은 다른 스레드를 실행 한 다음 작업이 끝나면 주 스레드에 알리는 것입니다.
지루하고 복잡하지만 작업이 완료됩니다. 그러나 Node는 어떻습니까? V8과 같은 Node.js가 단일 스레드이므로 확실한 문제가 발생합니다. 우리의 코드는 하나의 스레드에서만 실행될 수 있습니다.
브라우저에서 설정 setTimeout(someFunction, 0)
하면 때로는 마술처럼 해결할 수 있다고 들었을 것입니다. 하지만 시간 제한을 0으로 설정하고 실행을 0 밀리 초 지연하면 아무 문제가 해결되지 않는 이유는 무엇입니까? 그것은 단지 someFunction
즉시 전화하는 것과 같지 않습니까? 그렇지 않아.
우선, 호출 스택 또는 단순히 "스택"을 살펴 보겠습니다. 우리는 호출 스택의 기초를 이해하기 만하면되므로 간단하게 만들 예정입니다. 당신이 어떻게 작동하는지 잘 알고있는 경우 , 다음 섹션으로 자유롭게 뛰어 오십시오 .
스택
함수 반환 주소를 호출 할 때마다 매개 변수와 로컬 변수가 스택에 푸시됩니다. 현재 실행중인 함수에서 다른 함수를 호출하면 그 내용은 이전 주소와 동일한 방식으로 맨 위에 푸시됩니다.
간단히하기 위해, 나는 정확히 '정확하지는 않지만 지금부터는 함수가 스택 맨 위로 푸시된다'라고 말할 것이다.
한 번 보자!
1 function main () {
2 const hypotenuse = getLengthOfHypotenuse(3, 4)
3 console.log(hypotenuse)
4 }
5
6 function getLengthOfHypotenuse(a, b) {
7 const squareA = square(a)
8 const squareB = square(b)
9 const sumOfSquares = squareA + squareB
10 return Math.sqrt(sumOfSquares)
11 }
12
13 function square(number) {
14 return number * number
15 }
16
17 main()
main
먼저 호출됩니다.
3과 4를 인자로하여 getLengthOfHypotenuse를 호출한다.
이후 사각형은 a
사각형이 반환되면 스택에서 팝되고 반환 값이에 할당됩니다 squareA
. squareA가 스택 프레임에 추가됩니다.getLengthOfHypotenuse
똑같은 다음 호출을 위해 똑같이 간다.
다음 줄에서 표현식 squareA + squareB
이 평가됩니다.
sumOfSquares로 Math.sqrt가 호출됩니다.
이제 모든 것은 getLengthOfHypotenuse
계산의 최종 값을 반환하는 것입니다.
반환 값은에 할당됩니다 hypotenuse
.main
의 값은 hypotenuse
콘솔에 기록됩니다.
마지막으로 main
아무런 값도없이 반환되고, 스택에서 비워져 비어있게됩니다.
참고 : 함수 실행이 완료되면 스택에서 로컬 변수가 팝핑되는 것을 보았습니다. 숫자, 문자열 및 부울과 같은 간단한 값으로 작업 할 때만 발생합니다. 객체, 배열 등의 값은 힙에 저장되고 변수는 포인터에 불과합니다. 이 변수를 전달하면 포인터를 전달하여 다른 스택 프레임에서이 값을 변경할 수 있습니다. 함수가 스택에서 팝되면 객체에 대한 포인터 만 힙에 실제 값을 남기고 튀어 나오게됩니다. 가비지 컬렉터는 개체가 유용성을 잃으면 공간을 확보하는 사람입니다.
Node.js 이벤트 루프 입력
아니,이 루프가 아니야. :)
우리가 뭔가 같이 호출 할 때 어떤 일이 발생 setTimeout
, http.get
, process.nextTick
, 또는 fs.readFile
? 이 중 어느 것도 V8의 코드에서 찾을 수 없지만 Node.js의 경우 Chrome WebApi와 C ++ API에서 사용할 수 있습니다. 이를 이해하려면 실행 순서를 좀 더 잘 이해해야합니다.
더 일반적인 Node.js 애플리케이션 (서버에서 수신 대기)을 살펴 보겠습니다 localhost:3000/
. 요청을 받으면 서버는 wttr.in/<city>
날씨를 알아 내고 콘솔에 어떤 종류의 메시지를 인쇄하며 응답을받은 후 발신자에게 응답을 전달합니다.
'use strict'
const express = require('express')
const superagent = require('superagent')
const app = express()
app.get('/', sendWeatherOfRandomCity)
function sendWeatherOfRandomCity (request, response) {
getWeatherOfRandomCity(request, response)
sayHi()
}
const CITIES = [
'london',
'newyork',
'paris',
'budapest',
'warsaw',
'rome',
'madrid',
'moscow',
'beijing',
'capetown',
]
function getWeatherOfRandomCity (request, response) {
const city = CITIES[Math.floor(Math.random() * CITIES.length)]
superagent.get(`wttr.in/${city}`)
.end((err, res) => {
if (err) {
console.log('O snap')
return response.status(500).send('There was an error getting the weather, try looking out the window')
}
const responseText = res.text
response.send(responseText)
console.log('Got the weather')
})
console.log('Fetching the weather, please be patient')
}
function sayHi () {
console.log('Hi')
}
app.listen(3000)
요청이 전송 될 때 날씨를 얻는 것을 제외하고는 무엇이 인쇄 localhost:3000
됩니까?
Node에 대한 경험이 있다면 코드에서 console.log('Fetching the weather, please be patient')
후에 호출 되더라도 console.log('Got the weather')
전자가 먼저 인쇄됩니다.
Fetching the weather, please be patient
Hi
Got the weather
어떻게 된 거예요? V8이 단일 스레드인데도 Node의 기본 C ++ API는 그렇지 않습니다. 즉, 비 블로킹 동작을 호출 할 때마다 Node는 자바 스크립트 코드와 동시에 실행될 일부 코드를 호출합니다. 이 숨겨진 스레드가 기다리는 값을 받거나 오류를 throw하면 제공된 매개 변수를 사용하여 제공된 콜백이 호출됩니다.
참고 : 언급 한 'some code'는 실제로 libuv의 일부입니다 . libuv는 스레드 풀을 처리하고 시그널링 및 비동기 작업을 수행하는 데 필요한 다른 것을 처리하는 오픈 소스 라이브러리입니다. 그것은 원래 Node.js를 위해 개발되었지만 다른 많은 프로젝트 에서는 지금 사용하고 있습니다.
엔터프라이즈 급 Node.js 개발에 대한 도움이 필요하십니까?
RisingStack의 전문가를 고용하십시오!
후드를 들여다 보려면 이벤트 루프와 작업 큐라는 두 가지 새로운 개념을 도입해야합니다.
작업 대기열
Javascript는 단일 스레드, 이벤트 중심 언어입니다. 즉, 이벤트에 리스너를 연결할 수 있으며, 이벤트가 발생하면 리스너는 우리가 제공 한 콜백을 실행합니다.
당신이 전화를 할 때마다 setTimeout
, http.get
또는 fs.readFile
, Node.js를 우리의 코드를 실행 유지하기 위해 다른 스레드 수 V8에 이러한 작업을 보냅니다. 노드는 또한 카운터가 다운되거나 IO / http 작업이 끝나면 콜백을 호출합니다.
이러한 콜백은 다른 작업을 대기열에 추가 할 수 있으며 해당 함수는 다른 작업을 대기열에 넣을 수 있습니다. 이렇게하면 서버에서 요청을 처리하는 동안 파일을 읽은 다음 다른 요청을 처리하지 않고 읽은 내용을 기반으로 http 호출을 수행 할 수 있습니다.
그러나 우리는 단지 하나의 메인 쓰레드와 하나의 콜 스택을 가지고 있기 때문에, 그 파일이 읽힐 때 다른 요청이 제공 될 경우 콜백은 그 스택이 비워 질 때까지 기다려야 할 것이다. 콜백이 수행 될 차례를 기다리는 림보를 작업 대기열 (또는 이벤트 대기열 또는 메시지 대기열)이라고합니다. 주 스레드가 이전 작업을 마칠 때마다 콜백이 무한 루프에서 호출되므로 '이벤트 루프'라는 이름이 사용됩니다.
앞의 예제에서 다음과 같이 보일 것입니다 :
- 요청이 '/'에 도착할 때 호출 될 '요청'이벤트에 대한 핸들러를 명시 적으로 등록합니다.
- 기능을 건너 뛰고 포트 3000에서 청취를 시작합니다.
- 스택이 비어 있고 '요청'이벤트가 대기 중입니다.
- 들어오는 요청이있을 때, 오랫동안 기다렸던 이벤트가 시작되고, 제공된 핸들러가 명시 적으로 호출됩니다
sendWeatherOfRandomCity
sendWeatherOfRandomCity
스택으로 밀린다.getWeatherOfRandomCity
호출되어 스택으로 푸시됩니다.Math.floor
과Math.random
에서, 호출 스택에 푸시와 팝 있습니다cities
에 할당city
superagent.get
이 (가) 호출'wttr.in/${city}'
되면 처리기가end
이벤트에 대해 설정됩니다 .- 에 대한 http 요청
http://wttr.in/${city}
이 백그라운드 스레드로 보내지고 실행이 계속됩니다. 'Fetching the weather, please be patient'
콘솔에 기록됩니다getWeatherOfRandomCity
반환sayHi
라는 메시지'Hi'
가 콘솔에 인쇄됩니다.sendWeatherOfRandomCity
반환 값은 빈 상태로 남겨두고 스택에서 튀어 나옵니다.http://wttr.in/${city}
응답을 보내기를 기다리는 중- 응답이 도착하면
end
이벤트가 시작됩니다. anonymous handler
에 전달 우리.end()
라고는, 그보고의 값을 수정할 수 있습니다 의미의 폐쇄에있는 모든 변수를 스택에 푸시됩니다express, superagent, app, CITIES, request, response, city
우리가 정의한 모든 기능을response.send()
200
또는500
statusCode를 사용하여 호출 되지만, 다시 백그라운드 스레드로 보내 지므로 응답 스트림이 우리의 실행을 막지 못하고anonymous handler
스택에서 터집니다.
이제 이전에 언급 한 setTimeout
해킹이 왜 작동 하는지 이해할 수 있습니다 . 카운터를 0으로 설정하더라도 현재 스택과 작업 대기열이 비어있을 때까지 실행을 연기하여 브라우저가 UI를 다시 그리거나 다른 요청을 처리 할 Node를 다시 허용합니다.
마이크로 타스크 및 Macrotasks
이것이 충분하지 않다면 실제로는 하나 이상의 작업 대기열이 있습니다. 하나는 마이크로 타스크 용이고 다른 하나는 매크로 타스크 용입니다.
마이크로 작업의 예 :
process.nextTick
promises
Object.observe
매크로 태스크의 예 :
setTimeout
setInterval
setImmediate
I/O
다음 코드를 살펴 보겠습니다.
console.log('script start')
const interval = setInterval(() => {
console.log('setInterval')
}, 0)
setTimeout(() => {
console.log('setTimeout 1')
Promise.resolve().then(() => {
console.log('promise 3')
}).then(() => {
console.log('promise 4')
}).then(() => {
setTimeout(() => {
console.log('setTimeout 2')
Promise.resolve().then(() => {
console.log('promise 5')
}).then(() => {
console.log('promise 6')
}).then(() => {
clearInterval(interval)
})
}, 0)
})
}, 0)
Promise.resolve().then(() => {
console.log('promise 1')
}).then(() => {
console.log('promise 2')
})
그러면 콘솔에 기록됩니다.
script start
promise1
promise2
setInterval
setTimeout1
promise3
promise4
setInterval
setTimeout2
setInterval
promise5
promise6
WHATVG 사양 에 따르면 정확히 하나의 (매크로) 작업이 이벤트 루프의 한주기에서 매크로 타스크 큐에서 처리되어야합니다. 상기 매크로 태스크가 완료된 후에, 이용 가능한 모든 마이크로 태스크는 동일한 사이클 내에서 처리 될 것이다. 이러한 마이크로 태스크가 처리되는 동안, 마이크로 태스크 대기열이 고갈 될 때까지 하나씩 실행되는 더 많은 마이크로 태스크를 대기열에 넣을 수 있습니다.
이 다이어그램은 그림을 약간 뚜렷하게 만듭니다.
우리의 경우 :
주기 1 :
- `setInterval`이 태스크로 스케줄됩니다.
- `setTimeout 1`이 태스크로 스케줄됩니다.
- Promise.resolve 1 '에서'그때 '는 모두 마이크로 타스크로 계획됩니다
- 스택이 비어 있고 마이크로 태스크가 실행됩니다.
작업 대기열 : setInterval
,setTimeout 1
사이클 2 :
- 마이크로 태스크 큐는 비어 있고,`setInteval`의 핸들러가 실행될 수 있고, 또 다른`setInterval`은 태스크로서`setTimeout 1` 바로 뒤에 스케쥴됩니다
작업 대기열 : setTimeout 1
,setInterval
사이클 3 :
- 마이크로 태스크 큐는 비어 있고, 'setTimeout 1`의 핸들러가 실행될 수 있으며,'promise 3 '과'promise 4 '는 마이크로 태스크로 스케줄링되며,
- `promise 3`과`promise 4`의 핸들러가 실행됩니다.`setTimeout 2`가 태스크로 스케줄됩니다.
작업 대기열 : setInterval
,setTimeout 2
사이클 4 :
- 마이크로 태스크 큐는 비어 있고,`setInteval` 핸들러가 실행될 수 있고, 또 다른`setInterval`은 태스크로서,`setTimeout` 바로 뒤에 스케쥴됩니다
작업 대기열 : setTimeout 2
,setInteval
- `setTimeout 2`의 핸들러 실행,`promise 5`와`promise 6`은 마이크로 태스크로 스케줄됩니다.
지금의 핸들러 promise 5
및 promise 6
우리의 간격을 삭제 실행해야하지만, 어떤 이상한 이유로 setInterval
다시 실행됩니다. 그러나 Chrome에서이 코드를 실행하면 예상되는 동작이 발생합니다.
우리는 Node에서도 process.nextTick과 약간의 콜백 지옥으로 이것을 고칠 수 있습니다.
console.log('script start')
const interval = setInterval(() => {
console.log('setInterval')
}, 0)
setTimeout(() => {
console.log('setTimeout 1')
process.nextTick(() => {
console.log('nextTick 3')
process.nextTick(() => {
console.log('nextTick 4')
setTimeout(() => {
console.log('setTimeout 2')
process.nextTick(() => {
console.log('nextTick 5')
process.nextTick(() => {
console.log('nextTick 6')
clearInterval(interval)
})
})
}, 0)
})
})
})
process.nextTick(() => {
console.log('nextTick 1')
process.nextTick(() => {
console.log('nextTick 2')
})
})
이것은 우리의 사랑하는 약속이 사용하는 논리와 완전히 똑같지 만 조금 더 무시 무시합니다. 적어도 그것은 우리가 기대했던대로 일을 끝내게합니다.
비동기 !
우리가 보았 듯이 Node.js에 응용 프로그램을 작성할 때 작업 대기열과 이벤트 루프를 모두 관리하고주의를 기울여야합니다. 모든 기능을 활용하려는 경우, 장기 실행을 유지하려면 작업이 주 스레드를 차단하지 못하게합니다.
이벤트 루프는 처음에 파악하기에 미끄러운 개념 일 수 있지만, 일단 멈추게되면 그 이벤트 루프 없이는 인생이 있다는 것을 상상할 수 없습니다. 콜백 지옥으로 이어질 수있는 연속성 전달 스타일은보기 흉한 것처럼 보일 수 있지만 Promises가 있으며 곧 비동기식으로 대기하게됩니다. 우리가 기다리는 동안 비동기 - 대기 상태를 시뮬레이트 할 수 있습니다.
하나의 마지막 이별 권고 :
Node.js와 V8이 장시간 실행을 처리하는 방법을 알면 자신의 이익을 위해이를 사용할 수 있습니다. 그 전에는 긴 실행 루프를 작업 대기열로 보내야한다는 것을 들었을 것입니다. 당신은 손으로 그것을 할 수 있거나 async.js를 사용할 수 있습니다 .
해피 코딩!
질문이나 생각이 있으시거나, 의견을 말씀해 주시면, 제가 거기에있을 것입니다! Scale 시리즈의 Node.js 다음 부분에서는 Node.js의 가비지 콜렉션에 대해 논의 중이며 , 체크 아웃하는 것이 좋습니다!
'개발 > Node.JS' 카테고리의 다른 글
npm - npm Publishing Tutorial (0) | 2018.03.11 |
---|---|
npm - npm모범 사례 (0) | 2018.03.11 |
Node.js 이메일 보내기 (0) | 2018.03.03 |
Node.js 파일 시스템 모듈 (0) | 2018.03.03 |
Node.js HTTP 모듈 (0) | 2018.03.03 |