When, why, and how to multithread in Flutter

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
MICHAEL THOMSEN: Hi, I'm Michael Thomsen, and I am a product manager on the Dart and Flutter team. We're here today to tell you about when you can benefit from using concurrency in your Flutter apps. As a developer, you'll want to create apps with fast and smooth user interfaces with no UI jank. Sometimes other code can block the UI from updating. [PHONE RINGING] Sorry. Sometimes other code can block the UI from updating, triggering such UI jank. Concurrency can help you move that code so it runs in the background and no longer causes interruptions, like when someone interrupts you with a phone call in the middle of a video shoot. MICHAEL GODERBAUER: Hi, I'm Michael Goderbauer, and I'm an engineer on the Dart and Flutter team. As the other Michael said, concurrency is a great technique for running code in the background while keeping your app responsive and animations running smoothly. I'll show you today how you can offload heavy computations in your app to a background worker using isolates in Dart. Isolates are a construct for running multiple threads of execution concurrently. MICHAEL THOMSEN: Our first topic is synchronous versus asynchronous function calls and how this relates to the event loop of a Dart-based app. MICHAEL GODERBAUER: All Dart code runs in an isolate. Each isolate has a chunk of private memory and a single thread running an event loop. Lots of events happened during the lifetime of an app. Events are, for example, generated when the user taps the screen, I/O finishes loading, or when Flutter needs to repaint to update the UI shown on screen. As new events happen, they are added to the event queue. When the isolate runs, it grabs the events from the queue one by one and processes them on a single thread in its event loop. For smooth rendering, the Flutter framework adds a repaint request to the event queue 60 times a second, one for each frame. It's important that these are processed by the event loop on time. Otherwise, the app's UI will become unresponsive and animations will stutter. MICHAEL THOMSEN: Here's something I worked on recently, which illustrates this issue. This is a small app for doing image processing. You can load an image from disk or the network, and you can apply image effects like the sepia color filter. Let's take a look at the build method. First, we check if there's a current image. And if so, we render it. Then we have a progress indicator, which is shown when the show progress variable is true. This is used to provide feedback for long running operations like loading and image processing. Then we have a number of buttons, like the Load button. Let's take a look at the event handler for the Load button. This starts by showing a file picker, and then it does the actual loading of the image. As you can see here, then it starts by setting the show progress variable to true to enable the progress indicator. Then it reads the file, and then it unsets the show progress variable and updates the current image. Here's the running app. Let's start by loading an image. We'll select photograph and click OK. That's odd. We was supposed to get a progress indicator. We clearly set the show progress variable to true in the code, but it never appeared. Michael, can you explain what's going on here? MICHAEL GODERBAUER: Let's start by reviewing what happens behind the scenes when you load an image in this app. Once a file has been selected, the event loop of the main isolate is running the code of the load image function, which as you mentioned, sets show progress before it reads the bytes of the file, so you might expect that the UI would paint the progress indicator on the next repaint event. However, the read as byte sync call in the load image function runs synchronously and blocks the event loop until all bytes of the image have been loaded. This can take a few frames. The event loop doesn't get a chance to process the pending repaint event until after show progress has been unset again at the end of the load image method. As a result, we end up with an unresponsive app that misses a whole bunch of frames and we never get to see the progress indicator. If you change the file loading to await the result of an asynchronous read call, the loading of the file is done on a separate VM thread. While that thread is busy, the main isolate can continue to run and process repaint events to update the progress indicator animation. When the VM thread is done processing, it adds an event to the queue of the main isolate to signal that the file has been loaded. The main isolate then resumes the execution of the load function, which removes the progress indicator again. MICHAEL THOMSEN: Got it. Let me see if I can make that change to my app. We will start by reading the bytes using async. This returns a future, so we need to await it. Oh, and then whenever we use async, we need to add the async keywords to our function. Let's hard reload and try the load button again. We will take an image, click OK. Great, here's our progress indicator. And it looks like here's the loaded image. Thanks for helping me, Michael. A second feature I've been working on for my app is the ability to perform image processing, for example, to apply a sepia filter to add a warm color tone. Here's my current code. First, I set the show progress state true, then I do the image processing, update the current image, and set show progress to false. But that's odd. When I click the sepia button, it looks like the whole UI is frozen and nothing seems to be happening. There's certainly no progress. And wait, here's the finished image. What's happening now, Michael? MICHAEL GODERBAUER: Your app is doing the image processing synchronously in the main isolate again. The apply sepia function never yields to allow the event loop to process other events like drawing the progress indicator. The next time the event loop can process a repaint event is after show progress has been set to false again. Therefore, the progress indicator is never actually drawn on screen. MICHAEL THOMSEN: Based on the previous example, maybe I should try and use async. Let me try to wait for a future, for example, for the duration of, say, one second and see what happens. Oh, we need to add async keyword again. Let's hard reload and try that instead. We'll click the sepia button. Here's the-- wait, we got a little bit of the progress animation, but then the whole UI just froze again. MICHAEL GODERBAUER: In this attempt, the asynchronous Future.delayed API allows Dart to wait until the specified time has passed without blocking. In that time, the event loop can process other events, like repaints, and draw the progress indicator on screen. However, once the time has passed the code after the awaited future is scheduled to run in the main isolate again. While the filter is applied to the image in that block of code, the main isolate is blocked for a few frames and cannot service the repaint events on time to update the progress animation. Hence, we see a frozen animation on the screen. Just because an API is asynchronous doesn't mean its work is done concurrently. It can still end up blocking the main isolate, as we've seen here. Whenever you have work to do that cannot be completed between two frames, it's a good idea to offload the work to another thread to ensure that the main isolate can produce 60 frames per second for smooth user experience. In Dart, you can offload work to another threat by spawning a new isolate. This worker isolate then runs concurrently with the main isolate without blocking it. When it has completed its processing, it can return the result back to the main isolate. The easiest way to do this in Flutter is to use Flutter's built in compute function. This function will spawn an isolate, pass it some data to kick off a computation, and terminate the isolate once the computation is done. While the computation is running in the work isolate, the main isolate is free to process other events, like updating the animations shown on screen or responding to user input. MICHAEL THOMSEN: Great. Let me try that. First, we'll remove the future code, then we'll add the call to the compute function. I believe that takes two arguments, the function to call and then the state to pass to that function, which would be our image. Let's go ahead and clean this up, and we can hot reload. Oh wait, this returns a future, so we need to await it again. Now we can hot reload and try the sepia button. Great, here's our progress animation, and it appears to be running smoothly with no jank. And we get the processed image. Our image processing app demonstrated how to use the compute method to move a function to a background isolate. That's a great technique for short-lived functions, like processing a single image or parsing a large JSON blob. But sometimes, you have processing that runs continuously throughout the lifespan of an app. Michael, do you have an example of that? MICHAEL GODERBAUER: Yes. I've been working on a new game where you can play tic-tac-toe against Dash. However, whenever the advanced Dash AI powering the game is scheming up its next dashing move, the entire UI freezes and Dash stops dancing, as you can see here. To fix this, I need to move the game engine to its own isolate. That frees up the main isolate to keep the UI responsive and animations running smoothly. Since the game engine needs to continuously keep track of the game state, this is not suitable for a short-lived worker isolate. Instead, we need to keep the isolate running for the entire duration of the game. MICHAEL THOMSEN: In the image processing app, we applied a filter to the image with the compute function. This automatically handled creating a worker isolate, passed the input, and then returned the result and exited the isolate. This was an example of a short-lived isolate. For a long running isolate, we need to do the setup manually. First, we need to create the isolate. We can then pass messages between the main and worker isolates using ports. And finally, we can exit the isolate. Michael, what does that look like in your game? MICHAEL GODERBAUER: Here's an illustration of the architecture of my new and improved tic-tac-toe app. We still have a main isolate responsible for drawing the user interface. It spawns a long running worker isolate, which will own the game engine. When we need the game engine to compute the next move for Dash, we send a message to the worker isolate. While it is calculating the next move, the main isolate can continue to run so we avoid any interruptions or jank in the UI animations. When Dash move is ready, a message is sent back from the worker, which we can process in the main isolate to show that move on screen. This continues for each move until the game ends. Let's see what this looks like in actual code. When the game starts, we create a new concurrent game engine and the start method is called. This first spawns a new isolate passing at two parameters, the entry point for our worker isolate and its input. As input, we provide it with the sending endpoint of our receive port so the worker can send messages back to the main isolate. We've also wrapped the received port in a queue, so we can simply await the first message from the worker isolate. Typically, the first message is a send port, which allows us to send messages back to the worker. With the communication channels established, we send the first message to the isolate to start the game and await the initial UI state as response. The other methods of the game engine are implemented similarly. We forward the method call by sending a message to the isolate and await its response. The actual computation is done on the worker. This doesn't block the main isolate, so the UI with all animations continues to render smoothly at 60 frames per second. Let's shift our focus to the code for the worker isolate. Its entry point receives the send port we provided to the spawn method as input. This allows us to send messages back to the main isolate. To receive messages from the main isolate, we create a receive port and sent its send port back to the main isolate. Next, we instantiate the existing game engine and listen for incoming messages on the receive port. For each message, we call the corresponding game engine method and sent its return value back to the main isolate via the send port. That's it. Let's run the game with the modified engine again. As you can see, the animations now continue to run smoothly while our advanced Dash AI calculates its next dashing move concurrently on the worker isolate. MICHAEL THOMSEN: In summary, for Flutter apps, the main thread is responsible for rendering the user interface. To ensure that your UI runs smoothly without jank, your code must produce each frame within the frame budget at least 60 times a second, or even more frequently, on higher end phones, with faster refresh rates. Any code that takes longer to run than the frame budget will cause jank, and should be moved to another isolate. MICHAEL GODERBAUER: In this talk, we've shown three ways to run on another thread. For system APIs exposed in the .core libraries, use asynchronous calls awaiting the result. For short-lived background tasks, use the compute function. This takes care of spawning the worker isolate and handles the communication between the main and worker isolate. Finally, for a long running background tasks, use the isolate APIs to spawn a worker isolate. The isolate doesn't share any memory-- they are isolated. So to communicate between them, you send messages via ports. MICHAEL THOMSEN: You can find more information and both app examples using the links shown under Resources. Thanks for watching. We hope these tips will help you create user interfaces that run smoothly with no jank. [MUSIC PLAYING]
Info
Channel: Flutter
Views: 57,149
Rating: undefined out of 5
Keywords: Flutter, Dart isolate, event loop, dart runtime, multithreading, multithreading in Flutter, app development, app developers, flutter concurrency, concurrency, Google I/O, Google IO, I/O, IO, Google I/O 2022, IO 2022
Id: yUMjt0AxVHU
Channel Id: undefined
Length: 15min 10sec (910 seconds)
Published: Thu May 12 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.