JavaScript 병행-네트워크 관계자 설명

This is a repost from my personal blog

TL;박사 01 명

  • JavaScript는 단일 스레드이며 장시간 실행되는 스크립트로 인해 페이지가
  • 에 응답하지 않습니다.
  • WebWorkers는 별도의 스레드에서 JavaScript를 실행하여 메시지를 주 스레드와 통신할 수 있도록 합니다.
  • 대량의 데이터를 Darrays 또는 Array Buffers 형식으로 전송하는 메시지는 데이터가 복제되어 대량의 메모리 비용이 발생합니다.
  • 은 전송을 사용하여 클론의 메모리 비용을 줄이지만 송신자가 데이터에 액세스할 수 없게 함
  • 모든 코드에서 in this repository을 찾을 수 있습니다.
  • 은 JavaScript에서 수행하는 작업 유형에 따라 프로세서 간 분산 작업을 지원할 수 있습니다.
  • 예시 프로그램


    예를 들어, 우리는 웹 응용 프로그램을 구축하려고 한다. 이 프로그램은 표를 구성하는데, 항목마다 그 프로그램에 속하는 숫자가 prime인지 여부를 나타낸다.
    우리는 ArrayBuffer을 사용하여 우리의 브리 값을 보존할 것이다. 우리는 대담하게 그것을 10메가바이트 크기로 설정할 것이다.
    현재, 이것은 단지 우리의 스크립트로 하여금 힘든 일을 하게 할 뿐이다. 이것은 매우 유용한 일이 아니지만, 나는 미래의 게시물에서 여기에 기술된 기술을 사용하여 서로 다른 종류의 이진 데이터 (예를 들어 이미지, 오디오, 동영상) 를 처리할 것이다.
    여기서 우리는 매우 간단한 알고리즘을 사용할 것이다. (더 좋은 알고리즘을 사용할 수 있다.)
    function isPrime(candidate) {
      for(var n=2; n <= Math.floor(Math.sqrt(candidate)); n++) {
        // if the candidate can be divided by n without remainder it is not prime
        if(candidate % n === 0) return false
      }
      // candidate is not divisible by any potential prime factor so it is prime
      return true
    }
    
    다음은 우리 프로그램의 나머지 부분이다.

    색인html


    <!doctype html>
    <html>
    <head>
      <style>
        /* make the page scrollable */
        body {
          height: 300%;
          height: 300vh;
        }
      </style>
    <body>
      <button>Run test</button>
      <script src="app.js"></script>
    </body>
    </html>
    
    페이지를 스크롤하여 바로 JavaScript 코드의 효과를 볼 수 있습니다.

    응용 프로그램.js


    document.querySelector('button').addEventListener('click', runTest)
    
    function runTest() {
      var buffer = new ArrayBuffer(1024 * 1024 * 10) // reserves 10 MB
      var view = new Uint8Array(buffer) // view the buffer as bytes
      var numPrimes = 0
    
      performance.mark('testStart')
      for(var i=0; i<view.length;i++) {
        var primeCandidate = i+2 // 2 is the smalles prime number
        var result = isPrime(primeCandidate)
        if(result) numPrimes++
        view[i] = result
      }
      performance.mark('testEnd')
      performance.measure('runTest', 'testStart', 'testEnd')
      var timeTaken = performance.getEntriesByName('runTest')[0].duration
    
      alert(`Done. Found ${numPrimes} primes in ${timeTaken} ms`)
      console.log(numPrimes, view)
    }
    
    function isPrime(candidate) {
      for(var n=2; n <= Math.floor(Math.sqrt(candidate)); n++) {
        if(candidate % n === 0) return false
      }
      return true
    }
    
    우리는 User Timing API을 사용하여 시간을 측정하고 우리의 정보를 시간선에 추가합니다.
    이제 신뢰하는 오래된 Nexus 7(2013)에서 테스트를 실행하도록 하겠습니다.

    그래, 이것은 그다지 인상적이지 않다, 그렇지?
    더 심각한 것은 39초 동안 사이트가 어떤 것에 대한 반응도 멈추었다는 것이다. 스크롤, 클릭, 입력도 없었다.이 페이지는 동결되었다.
    이는 JavaScript가 단일 스레드이고 하나의 스레드에서 한 가지 일만 동시에 발생할 수 있기 때문입니다.더 심각한 것은 페이지의 상호작용과 관련된 거의 모든 내용 (따라서 스크롤, 텍스트 입력 등에 사용되는 브라우저 코드) 이 같은 라인에서 실행된다는 것이다.
    설마 우리는 어떤 번거로운 일도 할 수 없단 말인가?

    인터넷 종사자가 와서 구조하다


    아니오, 이것이 바로 우리가 Web Workers을 사용할 수 있는 일입니다.
    Web Worker는 JavaScript 파일로 웹 응용 프로그램 원본과 동일하며 별도의 스레드에서 실행됩니다.
    개별 스레드에서 실행된다는 것은 다음과 같습니다.
  • 동시 실행
  • 마스터 스레드 차단
  • 페이지가 응답하지 않음
  • DOM 또는 마스터 스레드
  • 의 변수 또는 함수에 액세스할 수 없습니다.
  • 은 네트워크를 사용할 수 있고 메시지
  • 을 사용하여 주 스레드와 통신할 수 있다
    그렇다면 주요 검색 작업이 진행될 때, 우리는 어떻게 페이지의 응답성을 유지합니까?다음은 프로그램입니다.
  • 일꾼을 시작하고 Array Buffer를
  • 에게 보냅니다.
    이 노동자는 자신의 일을 한다
  • 보조 스레드가 완성되면 Array Buffer와 찾은 소수를 주 스레드
  • 으로 보냅니다.
    다음은 업데이트된 코드입니다.

    응용 프로그램.js


    document.querySelector('button').addEventListener('click', runTest)
    
    function runTest() {
      var buffer = new ArrayBuffer(1024 * 1024 * 10) // reserves 10 MB
      var view = new Uint8Array(buffer) // view the buffer as bytes
    
      performance.mark('testStart')
      var worker = new Worker('prime-worker.js')
      worker.onmessage = function(msg) {
        performance.mark('testEnd')
        performance.measure('runTest', 'testStart', 'testEnd')
        var timeTaken = performance.getEntriesByName('runTest')[0].duration
        view.set(new Uint8Array(buffer), 0)
        alert(`Done. Found ${msg.data.numPrimes} primes in ${timeTaken} ms`)
        console.log(msg.data.numPrimes, view)
      }
      worker.postMessage(buffer)
    }
    

    제1노동자.js


    self.onmessage = function(msg) {
      var view = new Uint8Array(msg.data),
          numPrimes = 0
      for(var i=0; i<view.length;i++) {
        var primeCandidate = i+2 // 2 is the smalles prime number
        var result = isPrime(primeCandidate)
        if(result) numPrimes++
        view[i] = result
      }
      self.postMessage({
        buffer: view.buffer,
        numPrimes: numPrimes
      })
    }
    
    function isPrime(candidate) {
      for(var n=2; n <= Math.floor(Math.sqrt(candidate)); n++) {
        if(candidate % n === 0) return false
      }
      return true
    }
    
    다음은 Nexus 7을 다시 실행할 때 얻은 결과입니다.

    그럼, 응, 모든 의식이 우리에게 무엇을 주었니?어쨌든 지금은 얘가 더 느려졌어!
    이곳의 대승리는 그것을 더 빨리 만들지는 않았지만, 페이지를 스크롤하거나 다른 방식으로 상호작용을 시도해 보았다.그것은 시종일관 응답을 유지한다.계산이 자신의 라인으로 옮겨졌기 때문에, 우리는 주 라인 처리가 사용자에 대한 응답을 방해하지 않을 것입니다.
    그러나 우리가 더욱 속도를 내기 전에, 우리는 navigator.hardwareConcurrency이 어떻게 일을 하는지 중요한 세부 사항을 이해할 것이다.

    클론 비용


    앞에서 말한 바와 같이, 주 라인과 작업 라인은 분리되어 있기 때문에, 우리는 메시지를 사용하여 그들 사이에서 데이터를 전달해야 한다
    그러나 이것은 실제로 어떻게 그것들 사이에서 데이터를 이동합니까?우리가 이전에 한 방법의 답은 structured cloning이었다.
    이것은 우리가 10메가바이트의 Array Buffer를 일꾼에게 복제한 다음에 일꾼에게서 Array Buffer를 복제해야 한다는 것을 의미한다.
    나는 이것이 총 30MB의 메모리를 차지할 것이라고 가정한다. 10개는 원시적인 Array Buffer에서, 10개는 워크맨에게 보내는 복사본에서, 나머지 10개는 보내는 복사본에서.
    다음은 테스트를 시작하기 전의 메모리 사용량입니다.

    테스트 완료 후:

    잠깐만, 아직 50메가바이트가 있어.사실 증명:
  • Array Buffer
  • 에 대해 10mb부터 시작하겠습니다.
  • 클론 자체* 다른 +10mb
  • 생성
  • 클론을 +10mb로 복제하는 작업 스레드
  • 일꾼 다시 복제, +10mb
  • 클론 복제본을 마스터 스레드 +10mb 으로 복제
    *) 복제가 아닌 타겟 스레드로 클론을 이동하는 이유는 잘 모르겠지만 시리얼화 자체가 예상치 못한 메모리 오버헤드를 초래하는 것 같습니다.

    양도 가능한 설비는 시간을 절약할 수 있다


    다행히도 선택할 수 있는 두 번째 매개 변수인 postMessage에서는 서로 다른 방식으로 라인 사이에서 데이터를 전송하는데 이를 전송 목록이라고 부른다.
    두 번째 매개 변수는 클론에서 제외되고 이동되거나 전송되는 Transferable개의 객체 목록을 저장할 수 있습니다.
    그러나 전송 대상은 소스 스레드에서 중화되기 때문에 예를 들어 Array Buffer는 보조 스레드로 전송된 후 주 스레드에 어떠한 데이터도 포함하지 않으며 postMessage은 0이 됩니다.
    이것은 여러 라인이 공유 데이터에 접근할 때 일련의 문제를 처리하는 메커니즘을 실현해야 하는 비용을 피하기 위해서이다.
    전송을 사용하여 조정된 코드는 다음과 같습니다.

    응용 프로그램.js


    worker.postMessage(buffer, [buffer])
    

    제1노동자.js


      self.postMessage({
        buffer: view.buffer,
        numPrimes: numPrimes
      }, [view.buffer])
    
    다음은 우리의 숫자입니다.

    그래서 우리는 복제 노동자보다 조금 빨라서 원시적인 주선 차단 버전에 가깝다.우리 기억력 어때요?

    그래서 40mb부터 50mb가 넘으면 맞는 것 같아요.

    일꾼이 많을수록 속도가 빨라진다고?


    그래서 지금까지 저희가 이미...
  • 잠금 해제 메인 스레드
  • 클론
  • 에서 메모리 오버헤드 제거
    저희도 속도를 낼 수 있을까요?
    숫자의 범위와 버퍼를 여러 작업공간으로 분할하여 병렬로 실행하고 결과를 결합할 수 있습니다.

    응용 프로그램.js


    우리는 노동자를 한 명 내놓는 것이 아니라 네 명의 노동자를 내놓을 것이다.모든 노동자는 오프셋 시작과 검사할 숫자를 표시하는 메시지를 받을 것이다.
    노동자 한 명이 일을 끝낼 때, 그것은
  • 하나의 그룹 버퍼로 어떤 항목이 소수인지
  • 에 대한 정보를 포함합니다
  • 에서 발견된 소수 수량
  • 원시 편이량
  • 기본 길이
  • 그리고 우리는 데이터를 버퍼에서 목표 버퍼로 복사해서 찾은 소수의 총수를 구한다.
    모든 직원이 완성되면 우리는 최종 결과를 나타낼 것이다.
    document.querySelector('button').addEventListener('click', runTest)
    
    function runTest() {
      const TOTAL_NUMBERS = 1024 * 1024 * 10
      const NUM_WORKERS = 4
      var numbersToCheck = TOTAL_NUMBERS, primesFound = 0
      var buffer = new ArrayBuffer(numbersToCheck) // reserves 10 MB
      var view = new Uint8Array(buffer) // view the buffer as bytes
    
      performance.mark('testStart')
      var offset = 0
      while(numbersToCheck) {
        var blockLen = Math.min(numbersToCheck, TOTAL_NUMBERS / NUM_WORKERS)
        var worker = new Worker('prime-worker.js')
        worker.onmessage = function(msg) {
          view.set(new Uint8Array(msg.data.buffer), msg.data.offset)
          primesFound += msg.data.numPrimes
    
          if(msg.data.offset + msg.data.length === buffer.byteLength) {
            performance.mark('testEnd')
            performance.measure('runTest', 'testStart', 'testEnd')
            var timeTaken = performance.getEntriesByName('runTest')[0].duration
            alert(`Done. Found ${primesFound} primes in ${timeTaken} ms`)
            console.log(primesFound, view)
          }
        }
    
        worker.postMessage({
          offset: offset,
          length: blockLen
        })
    
        numbersToCheck -= blockLen
        offset += blockLen
      }
    }
    

    제1노동자.js


    작업 스레드는 주 스레드 정렬의 byteLength바이트를 수용할 수 있는 Uint8Array 보기를 만듭니다.
    기본 체크는 필요한 오프셋에서 시작하여 데이터를 다시 전송합니다.
    self.onmessage = function(msg) {
      var view = new Uint8Array(msg.data.length),
          numPrimes = 0
      for(var i=0; i<msg.data.length;i++) {
        var primeCandidate = i+msg.data.offset+2 // 2 is the smalles prime number
        var result = isPrime(primeCandidate)
        if(result) numPrimes++
        view[i] = result
      }
      self.postMessage({
        buffer: view.buffer,
        numPrimes: numPrimes,
        offset: msg.data.offset,
        length: msg.data.length
      }, [view.buffer])
    }
    
    function isPrime(candidate) {
      for(var n=2; n <= Math.floor(Math.sqrt(candidate)); n++) {
        if(candidate % n === 0) return false
      }
      return true
    }
    
    결과는 다음과 같습니다.


    따라서 이 해결 방안은 약 절반의 시간과 상당한 메모리 비용(40mb의 기본 메모리 사용량 +10mb의 목표 버퍼+4x2.5mb의 모든 작업공간 버퍼+2mb의 모든 작업공간 비용)을 썼다.
    다음은 근로자 4명의 애플리케이션을 사용하는 일정입니다.

    우리는 노동자들이 병행 운행하는 것을 볼 수 있지만, 우리는 4배의 속도를 얻지 못했다. 왜냐하면 어떤 노동자들은 다른 사람들보다 더 긴 시간을 필요로 하기 때문이다.이것은 우리가 숫자의 범위를 나누는 방식의 결과이다. 모든 노동자는 숫자 length을 2에서 x 사이의 모든 숫자로 나누어야 하기 때문에 숫자가 큰 노동자일수록 더 많은 구분, 즉 더 많은 작업을 해야 한다.이것은 당연히 더욱 평균적으로 조작을 분배하는 방식으로 숫자를 나누어 최소화할 수 있다.나는 이 열성적인 독자에게 이것을 연습으로 남겨 줄 것이다.
    또 다른 문제는 우리가 더 많은 노동자를 참여시킬 수 있느냐는 것이다.
    다음은 근로자 8명의 결과다.

    됐어, 속도가 느려졌어!타임라인은 이러한 상황이 발생한 이유를 보여줍니다.

    우리는 경미한 중첩을 제외하고는 4명의 노동자가 동시에 일하고 있다는 것을 발견했다.
    이것은 시스템과 직원의 특징에 따라 결정될 것이며, 딱딱한 숫자가 아니다.
    한 시스템이 같은 시간에 이렇게 많은 일을 할 수 밖에 없으며, 보통 입출력 제한 (즉 네트워크나 파일 처리량 제한) 이나 CPU 제한 (즉 CPU에서 계산을 실행하는 제한) 으로 일한다.
    우리의 예에서, 모든 노동자들은 우리가 계산한 CPU를 차지한다.Nexus 7에는 4개의 코어가 있으므로 CPU를 완전히 사용하는 네 명의 작업자를 동시에 처리할 수 있습니다.
    일반적으로 CPU 및 입출력(I/O)이 바인딩된 워크로드나 작은 워크로드에서 처리하기 어려운 문제가 발생할 수 있으므로 작업자의 수를 판단하기 어려울 수 있습니다.사용 가능한 논리 CPU 수를 알고 싶으면 √x 을 사용하십시오.

    마무리


    이것은 상당히 많은 내용이니, 우리 한번 되돌아봅시다.
    JavaScript는 단일 스레드이며 브라우저 작업과 동일한 스레드에서 실행되므로 UI를 신선하고 간결하게 유지할 수 있습니다.
    그리고 웹 워커를 사용하여 우리의 작업을 다른 라인으로 옮기고'postMessage'를 사용합니다.스레드 간 통신.
    우리는 라인이 무한히 확장되지 않을 것이라는 것을 알아차렸기 때문에, 우리가 운행하는 라인의 수량을 고려하는 것을 권장합니다.
    우리가 이렇게 할 때, 우리는 기본적으로 데이터가 복제되는 것을 발견하는데, 이것은 육안으로 보는 것보다 더 많은 메모리 소모를 초래하기 쉽다.
    우리는 데이터 전송을 통해 이 문제를 해결했는데 이것은 일부 유형의 데이터(Transferables)에 대해 실행 가능한 선택이다.

    좋은 웹페이지 즐겨찾기