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. 4.

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

    to senior developers at Fortune 100 companies — through their journey into software development.
  2. 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
  3. 7.

    A

  4. 8.
  5. 14.

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

    eatBreakfast() emails() googleHangout() Task #3 Task #2 Task #1
  6. 15.

    task3() Time task1() task2() Core #3 Core #2 Core #1

    eatBreakfast() emails() googleHangout() Task #3 Task #2 Task #1
  7. 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
  8. 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
  9. 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()
  10. 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()
  11. 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()
  12. 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()
  13. 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()
  14. 25.

    Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code parse() refresh() render()
  15. 26.

    Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code parse() refresh() render()
  16. 27.

    Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code parse() refresh() render()
  17. 28.

    Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code refresh() render() parse()
  18. 29.

    Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code render() parse() refresh()
  19. 30.

    Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code parse() refresh()
  20. 31.

    Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code refresh() parse()
  21. 32.

    Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code refresh()
  22. 33.

    Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code refresh()
  23. 34.

    Call Stack Web APIs Callback Queue Event Loop Fetch IndexedDB

    Timer stack.empty? && !queue.empty? Source Code
  24. 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
  25. 39.
  26. 40.

    1. Read File 2. Parse ID3 Metadata 3. Calculate Duration

    4. Import Album 5. Import Song — bit.ly/id3-parser
  27. 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
  28. 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) ]);
  29. 44.

    Async IIFEs • Async Immediately Invoked Function Expression (IIFE) let

    task = (async () => { let thing = await otherTask; let result = await doThings(thing); return result; })();
  30. 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;
  31. 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
  32. 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) =>
  33. 50.
  34. 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();
  35. 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 = () =>
  36. 59.

    let semaphore = Semaphore(4); let result = await semaphore( async

    () => { console.log('Acquired!'); return await importMP3(file); } );
  37. 60.

    let importMP3 = async (data) => /* … */ let

    limitedImportMP3 = limit(2, importMP3);
  38. 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
  39. 62.

    let importMP3 = async (data) => /* … */ let

    limitedImportMP3 = limit(2, importMP3); let limit = (max, fn) => { let semaphore = Semaphore(max); return (...args) => semaphore(() => fn(...args)); };
  40. 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)
  41. 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
  42. 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) => {
  43. 71.

    import { done } from './rpc'; (async () => {

    while (true) { let { data } = await once(self, 'message'); let song = await importMP3(data); done(song); } })();
  44. 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