Асинхронность

Лекция 03

Синхронный подход


console.log('job started');

let r = xyz();

console.log('result: ' + r);

console.log('job done');

function xyz() {
  // подготавливаем ответ
  return 42;
}
    

Асинхронный подход


console.log('job started');

xyz((r) => {
  console.log('result: ' + r);

  console.log('job done');
});

console.log('job in progress');

function xyz(cb) {
  // подготавливаем ответ
  setTimeout(() => { cb(42); }, 10);
}
    

Concurrency

Конкурентность

Parallelism

Параллельность



Multithreading

Многопоточность

Asynchrony

Асинхронность





+ / -

  • нет синхронизации, race, deadlock
  • меньше накладных расходов
  • идеально для UI и web-приложений
  • подход сложнее в освоении
  • сложнее отлаживать и читать

Однопоточность


// on click
let data = readFromDb();

function readFromDb() {
  // читаем долго из БД
  return data;
}
    

Многопоточность


// on click
let data = [];
let t = new Thread(readFromDb, data);

function readFromDb(data) {
  // читаем долго из БД
  // заполняем массив data
}
    

Асинхронность


// on click
let data = [];

readFromDb((result) => {
  // заполняем массив data из result
});

function readFromDb(cb) {
  // читаем долго из БД
  cb(data);
}
    

Закон Амдала


Callback

Функция обратного вызова

Callback


let forEach = function (items, algo) {
  for (let i = 0; i < items.length; i++) {
    algo(items[i]);
  }
};

let print = function (item) {
  console.log(item);
};

let send = function (item) {
  item.sendEmail();
};
    

Callback


let clients = [
  { email: 'jonhdoe@gmail.com',
    sendEmail: () => {} },
  { email: 'mrsmith@gmail.com',
    sendEmail: () => {} }
];

// выводим клиентов
forEach(clients, print);

// отправляем им email
forEach(clients, send);
    

Callback - использование

  • меняющийся алгоритм
  • оповещение о событии

Callback - + / -

  • повторное использование кода
  • изменение функциональности без изменения метода
  • создание цепочек вызовов
  • падение производительности
  • ухудшение читаемости

Callback - Node.js style


function callback(error, data) {
  if (error) {
    // обрабатываем ошибку
  } else {
    // обрабатываем данные
  }
}
    

Callback hell / Pyramid of Doom

Callback hell / Pyramid of Doom

http://callbackhell.com/


fs.readdir(dir, (err, files) => {
  if (err) { ... }

  files.forEach((name) => {
    if (!isImage(name)) return;

    fs.readFile(name, (err, image) => {
      if (err) { ... }

      compress(image, (err, comp) => {
        fs.writeFile(name, comp, (err) =>{
          if (err) { ... }
          else { ... }
        });
      });
    });
  });
});
    

1. Разбивайте на функции


fs.readdir(dir, processFiles);
    

1. Разбивайте на функции


function processFiles(err, files) {
  if (err) { ... }

  files.forEach(checkFile);
}
    

1. Разбивайте на функции


function checkFile(name) {
  if (!isImage(name)) return;

  fs.readFile(name, compressImage(name));
}
    

1. Разбивайте на функции


function compressImage(name) {
  return (err, image) => {
    if (err) { ... }
    compress(file, rewriteImage(name));
  }
}
    

1. Разбивайте на функции


function rewriteImage(name) {
  return (err, image) => {
    fs.writeFile(name, image, finishCompress);
  }
}
    

1. Разбивайте на функции


function finishCompress(err) {
  if (err) { ... }
  else { ... }
}
    

function processFiles(err, files) {
  if (err) { ... }

  files.forEach(checkFile);
}

function checkFile(name) {
  if (!isImage(name)) return;

  fs.readFile(name, compressImage(name));
}

function compressImage(name) {
  return (err, image) => {
    if (err) { ... }
    compress(file, rewriteImage(name));
  }
}

function rewriteImage(name) {
  return (err, image) => {
    fs.writeFile(name, image, finishCompress);
  }
}

function finishCompress(err) {
  if (err) { ... }
  else { ... }
}

fs.readdir(dir, processFiles);
    

2. Разбивайте на модули


...

function readDir(dir) {
  fs.readdir(dir, processFiles);
}

module.export = readDir;
    

const compressImagesDir
      = require('./compressImagesDir');

compressImagesDir('/tmp/images');
    

PubSub

Подписчик-Издатель, Event Emitter

PubSub


let news = {
  subs: [],
  onAdd: (cb) => {
    news.subs.push(cb);
  },
  add: (item) => {
    for (let i = 0; i < news.subs.length; i++)
      news.subs[i](item);
  }
};
    

PubSub


news.onAdd((item) => {
  console.log('CB#1:' + item);
});

news.onAdd((item) => {
  console.log('CB#2:' + item);
});

news.add('hot news!');

/*
  CB#1:hot news!
  CB#2:hot news!
*/
    

PubSub - шина


function Bus () {
  let self = this;

  self.events = {};

  self.on = (event, cb) => {
    if (!self.events[event])
      self.events[event] = [];

    self.events[event].push(cb);
  };

  ...
}
    

PubSub - шина


function Bus () {
  ...

  self.emit = (event, data) => {
    if (!self.events[event]) return;

    let cbs = self.events[event];

    for (let i = 0; i < cbs.length; i++)
      cbs[i](data);
  };
}
    

PubSub - шина


let bus = new Bus();

bus.on('newsAdded', (item) => {
  console.log('CB#1:' + item);
});
bus.on('newsAdded', (item) => {
  console.log('CB#2:' + item);
});
bus.on('xyz', (data) => { console.log(data); });

bus.emit('newsAdded', 'hot news!');

/*
  CB#1:hot news!
  CB#2:hot news!
*/
    

Promises

Promises/A+


function zipDir(dir) {
  return new Promise((resolve, reject) => {
    // архивируем директорию
    resolve(files);
  });
};

zipDir('/tmp/images')
  .then((files) => {
    // обрабатываем файлы
  });
    

Promises/A+


function zipDir(dir) {
  return new Promise((resolve, reject) => {
    // что-то пошло не так
    reject(err);
  });
};

zipDir('/tmp/images')
  .then((files) => { ... })
  .catch((err) => {
    // обрабатываем ошибку
  });
    

Promises/A+


function zipDir(dir) {
  return new Promise((resolve, reject) => {
    // что-то пошло не так
    reject(err);
  });
};

zipDir('/tmp/images')
  .then((files) => { ... }, (err) => { ... });
    

Promises/A+


// Deffered Object
function zipDir(dir) {
  return new Promise((resolve, reject) => {
    // Future
    resolve(files);
  });
};

// Promise
let p = zipDir('/tmp/images');

p.then((files) => { ... }, (err) => { ... });
    

Promises/A+ - состояние

  • pending / ожидание
  • fullfiled / выполнено
  • rejected / отклонено

Promises/A+ - обертка


function resolver(resolve, reject) {
  return (err, data) => {
    if (err) reject(err);
    else resolve(data);
  }
}

function readDir(dir) {
  return new Promise((res, rej) => {
    fs.readir(dir, resolver(res, rej));
  });
}
    

Promises/A+ - цепочка


function readDir(dir) {
  return new Promise((res, rej) => {
    // читаем файлы из папки
  });
}

function zip(files) {
  return new Promise((res, rej) => {
    // архивируем файлы
  });
}

function sendByEmail(file) {
  return new Promise((res, rej) => {
    // отправляем файл по email
  });
}
    

Promises/A+ - цепочка


readDir('/tmp/images')

  .then((files) => { return zip(files) })

  .then((file) => { return sendByEmail(file) })

  .then(() => { ... })

  .catch((err) => { ... });
    

Promises/A+ - All


readDir('/tmp/images')

  .then((files) => { return zip(files) })

  .then((file) => {
    return Promise.all([
      sendByEmail(user.email, file),
      sendByEmail(admin.email, file)
    ]);
  })

   // results - массив с данными от промиссов
  .then((results) => { ... })

  .catch((err) => { ... });
    

Promises/A+ - Race


readDir('/tmp/images')

  .then((files) => { return zip(files) })

  .then((file) => {
    return Promise.race([
      sendByEmail(user.email[0], file),
      sendByEmail(user.email[1], file)
    ]);
  })

   // result - данные от выполненного промиса
  .then((result) => { ... })

  .catch((err) => { ... });
    

Promises/A+ - особенности


const a = new Promise(...);

a.then((data) => ..., (error) => ...);

!=

a.then(...).catch(...);
    

Promises/A+ - особенности


const a = new Promise(...);

a.then(...);
a.then(...);

!=

a.then(...).then(...);
    

Promises/A+ - особенности


const a = new Promise(...);

a.then(...)
  .catch(...)
  .then(...)
  .catch(...);
    

Bluebird


http://bluebirdjs.com/
    

npm install bluebird
    

const Promise = require('bluebird');
    

Bluebird - Promisify


const fs = Promise.promisifyAll(require('fs'));

fs.readFileAsync('hero.txt', 'utf-8')
  .then((content) => console.log(content));
    

Bluebird - Spread


readDir('/tmp/images')

  .then((files) => { return zip(files) })

  .then((file) => {
    return Promise.all([
      sendByEmail(user.email, file),
      sendByEmail(admin.email, file)
    ]);
  })
  .spread((result1, result2) => { ... })
  .catch((err) => { ... });
    

Bluebird - Map / MapSeries


function downloadS3Directory(from, to) {
  return getS3DirectoryFiles(from)
  .then(names =>
    Promise.map(names, downloadS3File);
  )

  .then(files => files.map((file) => {
    file.name = file.name.replace(from, '');
    file.name = path.join(to, file.name);
    return file;
  }))

  .then(files =>
    Promise.mapSeries(files, saveFile)
  );
}
    

Bluebird - Some


Promise.some([
  ping("ns1.example.com"),
  ping("ns2.example.com"),
  ping("ns3.example.com"),
  ping("ns4.example.com")
], 2).then(pingArray => {
  console.log('OK');
});
    

Bluebird - Some


Promise.any([
  ping("ns1.example.com"),
  ping("ns2.example.com"),
  ping("ns3.example.com"),
  ping("ns4.example.com")
]).then(ping => {
  console.log('OK');
});
    

Bluebird - Spread


readDir('/tmp/images')

  .then((files) => { return zip(files) })

  .then((file) => {
    return Promise.all([
      sendByEmail(user.email, file),
      sendByEmail(admin.email, file)
    ]);
  })
  // .then([result1, result2]) => { ... })
  .spread((result1, result2) => { ... })
  .catch((err) => { ... });
    

Bluebird

  • finally - выполняется всегда
  • join - как all, только для фиксированного числа промисов
  • try - любая ошибка отклоняет промис

http://caniuse.com/

Async/Await

Async/Await


async function read() {
  let content = await
    fs.readFileAsync('hero.txt', 'utf-8');

  console.log(content);
};

read();
    

Async/Await


async function magic() {
  try {
    const files = await readDir('/tmp/images');

    const archive = await zip(files);

    await Promise.all([
      sendByEmail(user.email, file),
      sendByEmail(admin.email, file)
    ]);

    ...
  } catch (error) {
    ...
  }
}
    

Async/Await


(async function () {
  ...
})();
    

Async/Await - Babel


http://babeljs.io/
    

npm install --save-dev babel-core
    

npm install --save-dev babel-preset-es-2017
    

npm install --save babel-polyfill
    

https://babeljs.io/repl/
    

Async/Await - Babel

.babelrc


{
  "presets": ["es-2017"]
}
    

Async/Await - Babel

babel.js


const fs = require('fs');

require("babel-core")
  .transformFile('index.js', (err, result) => {
    let code = 'require("babel-polyfill");\n'
      + result.code;

    fs.writeFile('index.es5.js', code);
  });
    

Async/Await - Babel

index.es5.js