CS 134

Asynchrony: A Different Kind of Dance

  • Horse speaking

    Hay! I thought we were done with all this concurrency stuff. Now you're telling me there's something else?

  • PinkRobot speaking

    Not quite done! We've been looking at concurrency, where multiple things happen at the same time. Now we're going to look at asynchrony, which is… Well, let's explore that.

Concurrency vs. Asynchrony

In our journey through concurrency, we've seen how multiple processes or threads can run simultaneously, often requiring careful coordination. It's like a choreographed dance, where each dancer (thread) needs to know exactly when to move to avoid colliding with others.

Asynchrony, on the other hand, is more like a group of friends at a party. They might arrive at different times, chat with different people, and leave when they're ready. There's no strict choreography, just a general understanding of how to behave.

  • Duck speaking

    So, asynchrony is just messy concurrency?

  • PinkRobot speaking

    Not quite. It's more about focusing on the order of operations rather than simultaneous execution.

  • Goat speaking

    Meh. Sounds like it would be chaos.

  • Cat speaking

    How do we keep things organized?

  • PinkRobot speaking

    Good question! Let's look at an example to see how it works.

An Asynchronous Example

Let's consider a scenario where we're building a simple weather application. It needs to

  1. Fetch the user's location
  2. Use that location to fetch weather data
  3. Display the weather data

In a synchronous world, we'd do these steps one after the other, waiting for each to complete before moving on. But what if getting the location takes a while? Or if the weather service is slow? Our application would feel unresponsive.

Here's how we might handle these issues asynchronously using JavaScript and Promises:

// We'll use this function to simulate a delay, like the kind we'd have
// when fetching data from a server
///  - delay is a number of milliseconds to wait
///  - action is a function to call after the delay
function doLater(delay, action) {
    setTimeout(action, delay);      // JS has this built-in
}

function getUserLocation() {
    return new Promise((resolve) => {
        doLater(1000, () => resolve({ lat: 40.7128, lon: -74.0060 }));
    });
}

function getWeatherData(location) {
    return new Promise((resolve) => {
        doLater(1500, () => resolve({ temp: 72, condition: "Sunny" }));
    });
}

function displayWeather(weather) {
    console.log(`The weather is ${weather.temp}°F and ${weather.condition}`);
}

// Using our asynchronous functions
function fetchWeather() {
    return getUserLocation()
            .then(location => getWeatherData(location))
            .then(weather => displayWeather(weather))
            .catch(error => console.error("An error occurred:", error));
}

fetchWeather();

console.log("Weather app initialized!");

This code

  • Defines some functions, getUserLocation, getWeatherData, and displayWeather, that simulate fetching data asynchronously and acting on it, and a fetchWeather function that chains them together.
  • Calls fetchWeather() to start the process.
  • Logs Weather app initialized!
  • Hedgehog speaking

    Wait, what's with all these thens? And why does “Weather app initialized!” print first?

  • PinkRobot speaking

    Great observation! The thens are how we chain asynchronous operations.

  • BlueRobot speaking

    As for why “Weather app initialized!” prints first, that's because the asynchronous operations don't block the main thread. The program continues executing while waiting for the asynchronous operations to complete.

  • Horse speaking

    What about all the () => { ... } stuff?

  • PinkRobot speaking

    That's an arrow function in JavaScript—a concise way to define functions without giving them a name.

    In this case, we're using them to specify actions that will happen later. In the case of doLater, we use () => because when the function is finally invoked, it won't be passed any additional arguments. The action functions for Promise, .then, and .catch are also arrow functions, but in those cases the functions being called do take an argument.

A Promise represents the eventual completion (or failure) of an asynchronous operation, and the .then method attaches an action to be taken when that Promise is resolved. The chain of .then operations doesn't do anything when it's first defined; it just sets up what should happen when each Promise resolves (which takes a few seconds in this example).

When we create a Promise with code like

let pendingAnswer = new Promise((resolve) => {
    // Some code that eventually calls resolve with an answer
});

there is no guarantee as to exactly when the code block inside the Promise will run. It might run immediately, or it might run sometime later, depending on the situation.

  • Hedgehog speaking

    But what if we do care about when it runs? What if we need to wait for the result before moving on?

  • PinkRobot speaking

    Good question…

As we've presented it, you can't demand that the Promise resolve immediately. What we can do, however, is always attach another .then to the Promise that we're interested in, and that .then will only run after the Promise resolves.

  • Dog speaking

    So it's like ordering food at a restaurant. You place your order (start the asynchronous operation) and then do other things while waiting for your food (the result) to arrive?

  • PinkRobot speaking

    Exactly! And the .thens are like saying, “When my appetizer arrives, bring me the main course, and when that's done, bring dessert.”

  • Goat speaking

    Meh. Having to chain everything together with .then seems like a lot of work, and hard to follow.

Async/Await: A Synchronous Look at Asynchrony

JavaScript (and many other languages) have introduced async and await keywords to make asynchronous code look and behave more like synchronous code. Here's how we could rewrite our example using async and await:

async function fetchWeather() {
    try {
        const location = await getUserLocation();
        const weather = await getWeatherData(location);
        displayWeather(weather);
    } catch (error) {
        console.error("An error occurred:", error);
    }
}

This version looks like synchronous code, but it's actually just another way of writing the same fetchWeather function we had before. Both our original function and this one return a Promise that resolves when the function completes.

Key Concepts in Asynchrony

  1. Non-Blocking Operations: Asynchronous code doesn't wait for an operation to complete before moving on to the next line of code.
  2. Callbacks and Promises: These are ways to handle the result of an asynchronous operation once it's complete.
  3. Event Loop: In systems like Node.js, an event loop manages asynchronous operations, ensuring that they're handled efficiently without blocking the main thread.
  4. Async/Await: A more recent addition to many languages that makes asynchronous code look and behave more like synchronous code, while retaining the benefits of non-blocking operations.

Asynchrony vs. Concurrency: When to Use Which?

  • Use Concurrency when
    • You need precise control over the execution of multiple threads or processes.
    • You're dealing with shared resources that require careful synchronization.
    • You need multiple things happening over the same time period.
  • Use Asynchrony when
    • You're dealing with I/O-bound operations (e.g., network requests or file operations).
    • You want to keep a user interface responsive while performing background tasks.
    • You're more concerned with the order of operations than simultaneous execution.
  • Cat speaking

    This asynchrony stuff seems powerful, but also like it could get messy quickly. How do we keep it under control?

  • PinkRobot speaking

    That's a great point! Asynchronous code can indeed become complex. Some strategies to manage this complexity include

    1. Using async/await syntax for more readable code.
    2. Proper error handling with try/catch blocks.
    3. Avoiding deeply nested callbacks (often called “callback hell”).
    4. Using libraries or frameworks designed for managing asynchronous operations.
  • Duck speaking

    This reminds me of how we sometimes have to wait for slow system calls in OS programming. Is that related?

  • PinkRobot speaking

    Absolutely! In fact, many operating systems use asynchronous I/O to handle slow operations efficiently. The principles are very similar!

Conclusion

While concurrency is about doing multiple things at the same time, asynchrony is about starting a task, moving on to other tasks, and then handling the result of the first task when it's ready. Both approaches have their places in modern programming, and understanding when to use each can lead to more efficient and responsive applications.

Now that we've explored both concurrency and asynchrony, can you think of a real-world scenario where you might need to use both concurrency and asynchrony together? Describe the scenario and briefly explain how you would use each concept.

(When logged in, completion status appears here.)