$30 off During Our Annual Pro Sale. View Details »

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/

Jonathan Lee Martin

March 11, 2019
Tweet

More Decks by Jonathan Lee Martin

Other Decks in Programming

Transcript

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

    View Slide

  2. I’m Jonathan Lee Martin.
    @nybblr

    View Slide

  3. I teach developers
    to craft exceptional software.

    View Slide

  4. I’ve worked with over 300 developers —
    from career switchers to senior developers
    at Fortune 100 companies — through their
    journey into software development.

    View Slide

  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

    View Slide

  6. I travel the world
    as a landscape photographer.
    !

    View Slide

  7. A

    View Slide

  8. " in !

    View Slide

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

    View Slide

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

    View Slide

  11. task3()
    task1()
    task2()

    View Slide

  12. eatBreakfast()
    emails()
    googleHangout()

    View Slide

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

    View Slide

  14. task3()
    Time
    task1()
    task2()
    Thread #3
    Thread #2
    Thread #1
    eatBreakfast()
    emails()
    googleHangout()
    Task #3
    Task #2
    Task #1

    View Slide

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

    View Slide

  16. Did I… Mishear you?
    Sigh, one less excuse to dismiss JavaScript.

    View Slide

  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

    View Slide

  18. How does this magic work?
    Event Loop & Web APIs

    View Slide

  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

    View Slide

  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()

    View Slide

  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()

    View Slide

  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()

    View Slide

  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()

    View Slide

  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()

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  35. bit.ly/event-loop-help

    View Slide

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

    View Slide

  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

    View Slide

  38. Async IIFEs
    1. Declare concurrent dependencies with

    View Slide

  39. View Slide

  40. 1. Read File
    2. Parse ID3 Metadata
    3. Calculate Duration
    4. Import Album
    5. Import Song
    — bit.ly/id3-parser

    View Slide

  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

    View Slide

  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)
    ]);

    View Slide

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

    View Slide

  44. Async IIFEs
    • Async Immediately Invoked Function Expression (IIFE)
    let task = (async () => {
    let thing = await otherTask;
    let result = await doThings(thing);
    return result;
    })();

    View Slide

  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;

    View Slide

  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

    View Slide

  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) =>

    View Slide

  48. bit.ly/async-iifes

    View Slide

  49. Functional
    Programming
    2. Manage concurrency with

    View Slide

  50. View Slide

  51. Importing 01 Overture.mp3

    View Slide

  52. Importing 02 The Grid.mp3

    View Slide

  53. Importing 22 Finale.mp3

    View Slide

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

    View Slide

  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();

    View Slide

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

    View Slide

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

    View Slide

  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 = () =>

    View Slide

  59. let semaphore = Semaphore(4);
    let result = await semaphore(
    async () => {
    console.log('Acquired!');
    return await importMP3(file);
    }
    );

    View Slide

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

    View Slide

  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

    View Slide

  62. let importMP3 = async (data) => /* … */
    let limitedImportMP3 = limit(2, importMP3);
    let limit = (max, fn) => {
    let semaphore = Semaphore(max);
    return (...args) =>
    semaphore(() => fn(...args));
    };

    View Slide

  63. bit.ly/semaphorejs

    View Slide

  64. Web Workers
    3. Create your own threads with

    View Slide

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

    View Slide

  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)

    View Slide

  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

    View Slide

  68. let worker = new Worker('worker.js');
    worker.postMessage({ all: ['the', 'data']});
    worker.onmessage(({ data }) => {
    console.log(data);
    });

    View Slide

  69. let importMP3 = Cluster('mp3-worker.js');
    let song = await importMP3(
    songFile
    );

    View Slide

  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) => {

    View Slide

  71. import { done } from './rpc';
    (async () => {
    while (true) {
    let { data } = await once(self, 'message');
    let song = await importMP3(data);
    done(song);
    }
    })();

    View Slide

  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

    View Slide

  73. Always bet on JavaScript™

    View Slide

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

    View Slide