#dart#asynchrony

Asynchronous programming in Dart (Part I)

Sujay Prabhu's avatar

Sujay Prabhu

Dart is a single-threaded programming language. This means that Dart programs run in a single thread by default. How does Dart support asynchronous programming, then? Let us try to understand this in detail as we go along.

In the realm of asynchronous programming with Dart, you'll frequently encounter fundamental concepts such as Isolates, Event Loops, Event Queues, Futures, and the Async/Await mechanism and it is necessary to understand each of these term in detail. Note that Streams, another crucial aspect, will be covered comprehensively in a forthcoming article.

Isolate

Isolates are similar to threads or processes but do not share memory. Dart supports concurrency via Isolates. Each isolate has its own memory and a thread running event loops. Dart programs run in a single isolate by default, which is known as the main isolate. You can create new isolates using the Isolate.spawn() method which is often called background worker. Isolates communicate with each other using messages. The main isolate is where your program starts execution. Isolates are distinct from threads because they do not share memory. For example, if the application has a global variable, then each isolate will have its own copy of this variable. If that variable is updated in one isolate, the change will not be reflected in other isolates.

Event loop & Event queue

The event loop is responsible for asynchronous programming in Dart. It is a loop that continues running until there are no more events to process. If a piece of code takes a long time to execute, it can be executed asynchronously by the event loop. The event loop is the mechanism that continuously checks the event queue and executes events by taking them off the queue and processing them.

The event queue is a crucial component of the event loop. It holds the events that are to be processed by the event loop. The event loop picks up events from the event queue one by one and processes them. It is a single thread that continues running until there are no more events to process.

In addition to the event queue, there's also a microtask queue, which is similar but prioritized over the event queue. Tasks in the microtask queue are processed before those in the event queue, allowing finer control over the execution order of asynchronous operations.

EVENT_LOOP_DART

Future

A Future is a class that represents a computation that may result in the future. This is similar to the concept of promises in JavaScript.

Future mainly has 3 states: Uncompleted, Completed with a value and Completed with error.

Let us look into the code

void main() {
  print('A');
  print(resolveAfter2Seconds());
  print('C');
}
 
Future<String> resolveAfter2Seconds() {
  return Future.delayed(const Duration(seconds: 2), () {
    return 'Hello';
  });
}

What will be the output? Is it

A, Hello, C

or

A, C, Hello

The answer is neither. The output will be:

UNCOMPLETED_FUTURE

Why is that? Because the Future is not yet completed. It is still in the Uncompleted state. So, the print statement will print the instance of the Future object. Next question is, how can we get the value from the Future object?

void main() {
  print('A');
  resolveAfter2Seconds().then((res) {
    print(res);
  });
  print('C');
}

This would result in:

COMPLETED_FUTURE

The .then() method takes a callback function as an argument. This callback function will be called when the Future is completed. Hence you would see Hello printing on to screen after a delay of 2 seconds. To capture the error, we can use the .catchError() method.

void main() {
  print('A');
  resolveAfter2Seconds().then((res) {
    print(res);
  }).catchError((err) {
    print(err);
  });
  print('C');
}

Returning to the initial statement, "Dart is a single-threaded programming language." If Dart is single-threaded, how does it support asynchronous programming? The answer is the event loop.

Let us understand the event loop, event queue, and Future with an example:

http.get('https://jsonplaceholder.typicode.com/todos').then((response) {
  if (response.statusCode == 200) {
    print('Success!');
  }  
});

The above code makes a network request to jsonplaceholder to get the list of todos. There will be a delay in getting the response from the server depending on network speed, latency, etc. If Dart is single-threaded, then the main isolate will be blocked until the response is received from the server. This is not ideal. We want the main isolate to be free to do other things while the response is being fetched from the server. This is where the event loop comes into the picture.

The http.get('https://jsonplaceholder.typicode.com/todos') initiates an HTTP GET request. Being asynchronous, it returns a Future object and continues executing the next lines of code, if any. The callback function passed to .then() are stored in the Callback registry. Once the response is available, the Future associated with the HTTP request is completed. Upon completion of the Future, the registered callback is placed in the event queue. If there are no other events present in the event queue, the event loop will pick up the callback from the event queue. It checks the status of the call stack and, if it's empty, dequeues the callback from the event queue and pushes it onto the call stack and executes it. If the call stack is not empty, then the callback is pushed onto the call stack only after the call stack is empty.

Async/Await

Async/Await is a syntactic sugar for Future. It makes the code more readable and easier to understand. Let's see how we can rewrite the above code using Async/Await:

void main() async {
  print('A');
  var response = await http.get('https://jsonplaceholder.typicode.com/todos');
  if (response.statusCode == 200) {
    print('Success!');
  }  
}

The above code is much more readable and easy to understand. The await keyword makes the code wait until the Future is completed. The await keyword can only be used inside an async function. The async keyword is used to mark a function as async. If you recall, we used catchError with Future for error handling. How do we do that with Async/Await? It's simple. We use a try/catch block:

void main() async {
  try {
    var response = await http.get('https://jsonplaceholder.typicode.com/todos');
    if (response.statusCode == 200) {
      print('Success!');
    }  
  } catch (err) {
    print(err);
  }
}

In conclusion, Dart’s approach to asynchronous programming is both unique and powerful. By leveraging constructs like Isolates, Event Loops, Futures, and the Async/Await syntax, Dart elegantly supports asynchrony while maintaining the simplicity of a single-threaded environment. This enables developers to write efficient, non-blocking code that is crucial for modern applications, especially those requiring network operations or extensive data processing.

References