Async patterns to scale your multicore JavaScript… elegantly.

Async patterns to scale your multicore JavaScript… elegantly.

Code, blogs, videos and more: https://jonathanleemartin.com/

“JavaScript is a toy language because it doesn’t support multithreading.” Heard that one before? Although the event loop means our program does one thing at a time, JavaScript is actually well-suited for a plethora of concurrency problems while avoiding typical multithreading woes. You might say JavaScript is single-threaded… just so it can be multithreaded!

Using functional programming techniques with Async IIFEs, Web Worker clusters and SharedArrayBuffers, you can elegantly architecture highly concurrent multicore web apps and backends… without spaghetti.

Async IIFE write-up: https://jonathanleemartin.com/blog/cross-stitching-elegant-concurrency-patterns-for-javascript/
MP3 reader: https://jonathanleemartin.com/blog/encore-javascript-create-an-mp3-reader-with-dataviews-textdecoder/

27a38e420ceeb97e61f109c4c6a0e9b4?s=128

Jonathan Lee Martin

March 11, 2019
Tweet

Transcript

  1. scale your multicore JavaScript… elegantly. Async patterns to

  2. I’m Jonathan Lee Martin. @nybblr

  3. I teach developers to craft exceptional software.

  4. I’ve worked with over 300 developers — from career switchers

    to senior developers at Fortune 100 companies — through their journey into software development.
  5. Let’s do something together. jonathanleemartin.com Functional React 3 days Practical

    TDD 5 days Full Stack JavaScript 8 week Express in a Hurry 1 day Remote Collaboration 5 days Functional Programming in JS 3 days
  6. I travel the world as a landscape photographer. !

  7. A

  8. " in !

  9. " in ! to for Take a Make a ☎

    on " a
  10. to for Take a Make a ☎ on " a

    " in !
  11. task3() task1() task2()

  12. eatBreakfast() emails() googleHangout()

  13. eatBreakfast() Time emails() googleHangout() Task #3 Task #2 Task #1

  14. task3() Time task1() task2() Thread #3 Thread #2 Thread #1

    eatBreakfast() emails() googleHangout() Task #3 Task #2 Task #1
  15. task3() Time task1() task2() Core #3 Core #2 Core #1

    eatBreakfast() emails() googleHangout() Task #3 Task #2 Task #1
  16. Did I… Mishear you? Sigh, one less excuse to dismiss

    JavaScript.
  17. JavaScript is highly concurrent in Node and the browser. fetch('videos.json')

    setTimeout(refresh, 5000) db.transaction(['videos']) .objectStore('videos') .get('lotr-1') 2 1 3 4
  18. How does this magic work? Event Loop & Web APIs

  19. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer fetch('videos.json') .then(parse) setTimeout(refresh, 5000) db.transaction(['videos']) .objectStore('videos') .get('lotr-1') .then(render) stack.empty? && !queue.empty? Source Code
  20. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer fetch('videos.json') .then(parse) setTimeout(refresh, 5000) db.transaction(['videos']) .objectStore('videos') .get('lotr-1') .then(render) stack.empty? && !queue.empty? Source Code parse()
  21. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer fetch('videos.json') .then(parse) setTimeout(refresh, 5000) db.transaction(['videos']) .objectStore('videos') .get('lotr-1') .then(render) stack.empty? && !queue.empty? Source Code parse()
  22. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer setTimeout(refresh, 5000) db.transaction(['videos']) .objectStore('videos') .get('lotr-1') .then(render) stack.empty? && !queue.empty? Source Code parse() refresh()
  23. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer setTimeout(refresh, 5000) db.transaction(['videos']) .objectStore('videos') .get('lotr-1') .then(render) stack.empty? && !queue.empty? Source Code parse() refresh()
  24. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer db.transaction(['videos']) .objectStore('videos') .get('lotr-1') .then(render) stack.empty? && !queue.empty? Source Code parse() refresh() render()
  25. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code parse() refresh() render()
  26. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code parse() refresh() render()
  27. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code parse() refresh() render()
  28. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code refresh() render() parse()
  29. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code render() parse() refresh()
  30. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code parse() refresh()
  31. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code refresh() parse()
  32. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code refresh()
  33. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code refresh()
  34. Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code
  35. bit.ly/event-loop-help

  36. Just use multiple processes! PM2, require('cluster')

  37. Today, we’ll cover: 1. Declare concurrent dependencies with Async IIFEs

    2. Manage concurrency with Functional Programming 3. Create your own threads with Web Workers
  38. Async IIFEs 1. Declare concurrent dependencies with

  39. None
  40. 1. Read File 2. Parse ID3 Metadata 3. Calculate Duration

    4. Import Album 5. Import Song — bit.ly/id3-parser
  41. // Read the file let buffer = await read(file); //

    Parse out the ID3 metadata let meta = await parser(file); let songMeta = mapSongMeta(meta); let albumMeta = mapAlbumMeta(meta); // Compute the duration let duration = await getDuration(buffer); // Import the album let albumId = await importAlbum(albumMeta); // Import the song let songId = await importSong({ ...songMeta, albumId, file, duration, meta }); return songId; 1. Read File 2. Parse ID3 Metadata 3. Calculate Duration 4. Import Album 5. Import Song
  42. let [ buffer, meta ] = await Promise.all([ // Read

    the file read(file), // Parse out the ID3 metadata parser(file) ]); let songMeta = mapSongMeta(meta); let albumMeta = mapAlbumMeta(meta); let [ duration, albumId ] = await Promise.all([ // Compute the duration getDuration(buffer), // Import the album importAlbum(albumMeta) ]);
  43. Async IIFEs • Async Immediately Invoked Function Expression (IIFE) (async

    () => { /* do things */ })();
  44. Async IIFEs • Async Immediately Invoked Function Expression (IIFE) let

    task = (async () => { let thing = await otherTask; let result = await doThings(thing); return result; })();
  45. let readTask = read(file); // Parse out the ID3 metadata

    let metaTask = (async () => { let meta = await parser(file); let songMeta = mapSongMeta(meta); let albumMeta = mapAlbumMeta(meta); return { meta, songMeta, albumMeta }; })(); // Import the album let albumImportTask = (async () => { let { albumMeta } = await metaTask; let albumId = await importAlbum(albumMeta); return albumId;
  46. let readTask = read(file); // Parse out the ID3 metadata

    let metaTask = (async () => { let meta = await parser(file); let songMeta = mapSongMeta(meta); let albumMeta = mapAlbumMeta(meta); return { meta, songMeta, albumMeta }; })(); // Import the album let albumImportTask = (async () => { let { albumMeta } = await metaTask; let albumId = await importAlbum(albumMeta); return albumId; })(); // Compute the duration let durationTask = (async () => { let buffer = await readTask; let duration = await getDuration(buffer); return duration; })(); // Import the song let songImportTask = (async () => { let albumId = await albumImportTask; let { meta, songMeta } = await metaTask; let duration = await durationTask; let songId = await importSong({ ...songMeta, albumId, file, duration, meta }); return songId; })(); let songId = await songImportTask; return songId; metaTask readTask durationTask albumImportTask songImportTask return songId
  47. let readTask = read(file); // Parse out the ID3 metadata

    let metaTask = (async () => { let meta = await parser(file); let songMeta = mapSongMeta(meta); let albumMeta = mapAlbumMeta(meta); return { meta, songMeta, albumMeta }; })(); // Import the album let albumImportTask = (async () => { let { albumMeta } = await metaTask; let albumId = await importAlbum(albumMeta); return albumId; })(); // Compute the duration let durationTask = (async () => { let buffer = await readTask; let duration = await getDuration(buffer); return duration; })(); // Import the song let songImportTask = (async () => { let albumId = await albumImportTask; let { meta, songMeta } = await metaTask; let duration = await durationTask; let songId = await importSong({ ...songMeta, albumId, file, duration, meta }); return songId; })(); let songId = await songImportTask; return songId; metaTask readTask durationTask albumImportTask songImportTask return songId (file) =>
  48. bit.ly/async-iifes

  49. Functional Programming 2. Manage concurrency with

  50. None
  51. Importing 01 Overture.mp3

  52. Importing 02 The Grid.mp3

  53. Importing 22 Finale.mp3

  54. let semaphore = new Semaphore(4); await semaphore.acquire(); /* do things

    */ semaphore.release();
  55. class Semaphore { constructor(max) { this.tasks = []; this.counter =

    max; this.dispatch = this.dispatch.bind(this); } dispatch() { if (this.counter > 0 && this.tasks.length > 0) { this.counter--; this.tasks.shift()(); } } release() { this.counter++; this.dispatch();
  56. let semaphore = new Semaphore(4); await semaphore.acquire(); /* do things

    */ semaphore.release();
  57. let semaphore = new Semaphore(4); await semaphore.acquire(); throw new Error('BOOM');

    semaphore.release();
  58. let Semaphore = (max) => { let tasks = [];

    let counter = max; let dispatch = () => { if (counter > 0 && tasks.length > 0) { counter--; tasks.shift()(); } }; let release = () => { counter++; dispatch(); }; let acquire = () =>
  59. let semaphore = Semaphore(4); let result = await semaphore( async

    () => { console.log('Acquired!'); return await importMP3(file); } );
  60. let importMP3 = async (data) => /* … */ let

    limitedImportMP3 = limit(2, importMP3);
  61. let importMP3 = async (data) => /* … */ let

    limitedImportMP3 = limit(2, importMP3); limitedImportMP3(song1); // starts immediately limitedImportMP3(song2); // starts immediately limitedImportMP3(song3); // waits for song1 or song2 to finish
  62. let importMP3 = async (data) => /* … */ let

    limitedImportMP3 = limit(2, importMP3); let limit = (max, fn) => { let semaphore = Semaphore(max); return (...args) => semaphore(() => fn(...args)); };
  63. bit.ly/semaphorejs

  64. Web Workers 3. Create your own threads with

  65. Fetch setTimeout WebRTC IndexedDB Audio FileReader Bluetooth Crypto ServiceWorker WebSocket

    WebWorker
  66. Audio IndexedDB setTimeout Fetch Thread Pool (x8) Main Thread fetch('videos.json')

    setTimeout(refresh, 5000) db.transaction(['videos']) .objectStore('videos') .get('lotr-1') ctx.decodeAudioData(buffer)
  67. Audio IndexedDB setTimeout Fetch Thread Pool (x8) Main Thread importMP3(file)

    fetch('videos.json') setTimeout(refresh, 5000) db.transaction(['videos']) .objectStore('videos') .get('lotr-1') ctx.decodeAudioData(buffer) MySongImporter
  68. let worker = new Worker('worker.js'); worker.postMessage({ all: ['the', 'data']}); worker.onmessage(({

    data }) => { console.log(data); });
  69. let importMP3 = Cluster('mp3-worker.js'); let song = await importMP3( songFile

    );
  70. let maxWorkers = navigator.hardwareConcurrency || 4; let defaultHandler = async

    (worker, data) => { worker.postMessage(data); return await once('message'); }; let Cluster = ( path, handler = defaultHandler, max = maxWorkers ) => { let pool = []; let semaphore = Semaphore(max); let useWorker = async (fn) => {
  71. import { done } from './rpc'; (async () => {

    while (true) { let { data } = await once(self, 'message'); let song = await importMP3(data); done(song); } })();
  72. TL;DR: 1. JavaScript is highly concurrent 2. Thread-safety isn’t a

    thing 3. Functional programming keeps it elegant 4. Make your own async APIs with Web Workers
  73. Always bet on JavaScript™

  74. Let’s do something together. jonathanleemartin.com