'Dart - make long running synchronous function asynchronous
I have a function that might take a few seconds to execute, and it is synchronous. Does:
String slowFunction() { ... }
...
Future<String>(() => slowFunction());
change it to asynchronous?
If I need its result in the next step, does this code make sense?
Future<void> anotherFunction() async {
// other async calls with Futures and await
...
final result = await Future<String>(() => slowFunction());
print(result);
...
// do something else with result
}
Seems kind of strange to create a Future
only to immediately await
on it. Should I just call the function? I guess it kind of 'yields' and allows other code to be executed before, but does such code have any use?
Solution 1:[1]
There is no point in making a process that is inherently synchronous and dressing it up as an asynchronous one. This is due to how asynchronicity (more generally referred to as "concurrency") works, not just in Dart but in general. Concurrency is just a programming trick to make multiple operations run interleaved with each other in the same thread, giving the illusion of true parallelism (which is where different threads or processes run simultaneously). This allows processes that would normally block while waiting for a resource to be put off until later as the program carries on with other things.
If you were to take a synchronous process that blocks due to work being actively done, either the program will block anyway as the "async" code executes just as it would otherwise do or the program will block just as long but at a later time. Either way, you are still blocking your program with a long-running process.
Take the following for example, which is what you ask: take a long-running process and wrap it in a Future
, thus making it "asynchronous":
String slowFunction() { ... }
...
String result = await Future(slowFunction);
In normal concurrency, this will put slowFunction
in the async queue. The next time the program has some downtime (in between UI draw calls, for example) it will pull that function out of the queue and process it. And thats when it will block for 2-3 seconds while the function executes.
In Dart, though, it works slightly differently. Because slowFunction
is not an async
function and doesn't await
anything, Dart will attempt to run it synchronously anyway, in which case you needn't have bothered wrapping it in a Future
in the first place.
You have two options here if you want to break up the operation of your synchronous function. Either you have to break it up into distinct operations that you can await
between (which is itself a somewhat complicated process, isn't always possible, and is generally a good source of code smell), or you offload the function to a separate thread altogether, employing parallelism rather than mere concurrency.
Dart is single-threaded, but it can be multi-processed through the use of isolates. (An isolate is Dart's name for a child process and is as close to true multithreading that you can get in Dart.) By wrapping your function in an Isolate
, you can run the work on an entirely separate process. That way, if that process blocks for 2-3 seconds, it won't affect the bulk of your app at all.
There is a catch, though. Because isolates are completely different processes, there is no sharing of memory whatsoever. That means any data that the isolate has access to has to be manually passed in through the use of "ports", namely SendPort
and ReceivePort
. This naturally makes isolate programming a bit of a pain, but in exchange, you won't run into things like your program having race conditions or getting deadlocked. (Because of shared memory problems, at least. Strictly speaking, there are plenty of other ways to get deadlocks and race conditions.)
Using an Isolate
works like so:
// main process
void createIsolate() async {
ReceivePort isolateToMain = ReceivePort();
isolateToMain.listen((data) {
// Listen for data passed back to the main process
});
Isolate myIsolateInstance = await Isolate.spawn(myIsolate, isolateToMain.sendPort);
}
// isolate process
void myIsolate(SendPort mainToIsolate) {
final result = slowFunction();
mainToIsolate.send(result);
}
Solution 2:[2]
I have a function that might take a few seconds to execute, and it is synchronous. Does:
String slowFunction() { ... } ... Future<String>(() => slowFunction());
change it to asynchronous?
Simply returning a Future
will not make your function asynchronous in the way that you probably want.
When you call an asynchronous function, as much of it as possible will be executed synchronously. That is, the function will execute synchronously until it reaches the first await
(i.e., until it returns a Future
). Since a Dart isolate is single-threaded, if you want other work to be able to occur concurrently with your long-running operation, slowFunction
would internally need to use await
(which is syntactic sugar for creating Future.then()
callbacks) to allow execution to yield.
Consider the following code:
Future<void> longRunningOperation1() async {
for (var i = 0; i < 100000000; i += 1) {
if (i % 10000000 == 0) {
print('longRunningOperation1: $i');
}
}
}
Future<void> longRunningOperation2() async {
for (var i = 0; i < 100000000; i += 1) {
if (i % 10000000 == 0) {
print('longRunningOperation2: $i');
}
}
}
Future<void> main() async {
await Future.wait([longRunningOperation1(), longRunningOperation2()]);
}
You will see that longRunningOperation1
and longRunningOperation2
never overlap; one always runs to completion before the other one starts. To allow the operations to overlap with minimal changes, you could do:
Future<void> longRunningOperation1() async {
for (var i = 0; i < 100000000; i += 1) {
if (i % 10000000 == 0) {
print('longRunningOperation1: $i');
await Future.delayed(Duration.zero);
}
}
}
Future<void> longRunningOperation2() async {
for (var i = 0; i < 100000000; i += 1) {
if (i % 10000000 == 0) {
print('longRunningOperation2: $i');
await Future.delayed(Duration.zero);
}
}
}
Solution 3:[3]
I am using a wrapper to spawn slow operations into a separate Isolate
and return aFuture
. It also allows to pass the function to run and some arguments as well.
import 'dart:async';
import 'dart:isolate';
/// Example
///
/// ```
/// main() async {
/// String str;
/// str = await runAsync<String, String Function(String)>(sing, ["lalalala"]);
/// print(str);
///
/// str = await runAsync<String, Function>(song);
/// print(str);
/// }
///
/// String sing(String str) => "Singing: " + str;
/// String song() => "lololololo";
/// ```
Future<R> runAsync<R, F>(F func, [List<dynamic> parameters]) async {
final receivePort = ReceivePort();
await Isolate.spawn(asyncRunner, receivePort.sendPort);
// The 'asyncRunner' isolate sends it's SendPort as the first message
final sendPort = await receivePort.first;
final responsePort = ReceivePort();
sendPort.send([responsePort.sendPort, func, parameters ?? []]);
final res = await responsePort.first;
if (res is! R)
return Future.error(res);
else if (res == null) return null;
return res as R;
}
// Isolate entry point
void asyncRunner(SendPort sendPort) async {
// Open the ReceivePort for incoming messages
final port = ReceivePort();
// Notify our creator the port we listen to
sendPort.send(port.sendPort);
final msg = await port.first;
// Execute
final SendPort replyTo = msg[0];
final Function myFunc = msg[1];
final List<dynamic> parameters = msg[2] ?? [];
try {
switch (parameters.length) {
case 0:
replyTo.send(myFunc());
break;
case 1:
replyTo.send(myFunc(parameters[0]));
break;
case 2:
replyTo.send(myFunc(parameters[0], parameters[1]));
break;
case 3:
replyTo.send(myFunc(parameters[0], parameters[1], parameters[2]));
break;
case 4:
replyTo.send(
myFunc(parameters[0], parameters[1], parameters[2], parameters[3]));
break;
case 5:
replyTo.send(myFunc(parameters[0], parameters[1], parameters[2],
parameters[3], parameters[4]));
break;
default:
replyTo.send(Exception("Unsupported argument length"));
}
} catch (err) {
replyTo.send(Exception(err.toString()));
}
// Done
port.close();
Isolate.current.kill();
}
https://github.com/vocdoni/dvote-dart/blob/main/lib/util/asyncify.dart#L16
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | |
Solution 3 | Dharman |