Страницы

воскресенье, 15 января 2017 г.

Node.js Event Loop

А знаете ли вы, что process.nextTick и Promise используют отдельный Event Loop внутри Event Loop, а fs, crypto.randomBytes и crypto.pbkdf2, dns.lookuphttp.get и http.request (вызванные с именем хоста, а не ip-адресом, и поэтому вызывающие dns.lookup), а также C/C++ аддоны, которые используют libuv thread pool, создают свои собственные потоки (по умолчанию до 4, максимум до 128), и таким образом Node.js Event Loop не совсем Single Threaded, а скорее Mostly Single Threaded?

В качестве аперитива попробуем угадать что выведет в консоль следующий код:
- loop-promise.js:
'use strict';
 
const interval = setInterval(() => {
  console.log('\nsetInterval');
}, 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');
  });

- loop-next-tick.js:
'use strict';
 
const interval = setInterval(() => {
  console.log('\nsetInterval');
}, 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');
  });
});

Несколько секунд напрягаем извилины...


... я не угадал.

Переходим к основному блюду. Создадим C/C++ аддонnode-gyp у меня уже установлен. C/C++ не моя тема, поэтому установим Native Abstractions for Node.js:

Пишем код аддона:
- addon.cc:
#include <nan.h>  // includes v8
 
using namespace Nan;
using namespace v8;
 
class IncWorker : public AsyncWorker {
  public:
    IncWorker(Callback *callback, int i) 
      : AsyncWorker(callback), i(i) {} 
    ~IncWorker() {}
    void Execute () {
      ++i;
    }
    void HandleOKCallback () {
      Nan::HandleScope scope;
 
      Local<Value> argv[] = {
        Null(), 
        New<Number>(i)
      };
      callback->Call(2, argv);
    }
  private:
    int i;
};
 
NAN_METHOD(Inc) {
  int i = To<int>(info[0]).FromJust();
  Callback *callback = new Callback(info[1].As<Function>());
  AsyncQueueWorker(new IncWorker(callback, i));
}
 
NAN_METHOD(IncSync) {
  int i = To<int>(info[0]).FromJust();
  info.GetReturnValue().Set(++i);
}
 
NAN_MODULE_INIT(Init) {
    Nan::Set(target, New<String>("inc").ToLocalChecked(),
      GetFunction(New<FunctionTemplate>(Inc)).ToLocalChecked());
    Nan::Set(target, New<String>("incSync").ToLocalChecked(),
      GetFunction(New<FunctionTemplate>(IncSync)).ToLocalChecked());
}
 
NODE_MODULE(addon, Init)

Создаем файл конфигурации аддона:
binding.gyp:
{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "addon.cc" ],
      "include_dirs" : ["<!(node -e \"require('nan')\")"],
    }
  ]
}

Генерируем файлы билда:

Билдим:

Тестируем:
- addon.js:
'use strict';
 
const addon = require('./build/Release/addon');
 
addon.inc(21, (err, result) => {
  console.log('inc:', result);
});
console.log('incSync:', addon.incSync(11));


Для наглядности добавим в асинхронный код задержку выполнения 100 миллисекунд (Sleep(100)):
- addon.cc:
//----- class IncWorker
void Execute () {
  Sleep(100);
  ++i;
}
//-----

Билдим заново:

Тестируем...
- addon.js:
'use strict';
 
const addon = require('./build/Release/addon');
 
const log = (i, arr) => console.log(`${i}\t${(arr[0] * 1e3 + arr[1] * 1e-6).toFixed(3)} ms`);
 
const fn = (i, arr) => {
  return new Promise((resolve, reject) => {
    addon.inc(i, (err, result) => {
      if (err) return reject(err);
      log(result, process.hrtime(arr));
      resolve();
    });
  });
};
 
const i = 16;
const arr = process.hrtime();
 
Promise.all([...new Array(i)].map((_, i) => fn(i, arr)))
  .then(() => {
    process.exit(0);
  })
  .catch((err) => {
    console.error(err);
    process.exit(1);
  });


 ...libuv thread pool in action!

Создаем переменную окружения по имени UV_THREADPOOL_SIZE:

Тестируем...

... spooky :)

Ссылки:
Node's Event Loop From the Inside Out by Sam Roberts, IBM
Everything You Need to Know About Node.js Event Loop - Bert Belder, IBM
- On problems with threads in node.js
Developer initiates I/O operation. You won't believe what happens next
Understanding the Node.js Event Loop - Node.js at Scale
- Building an Asynchronous C++ Addon for Node.js using Nan