24

I really like the async/await pattern in Dart. It allows me to write readable methods.

But, there are a couple of things that are problematic, one in particular, I don't know hot to manage at all.

The problem is that with async and multiple await inside a method, we introduce concurrency in the method. For example If I have a method:

Future<int> foo(int value) async {
await foo2();
await foo3();
await foo4();
int ret = foo5(value);
return ret;
}

Well, this is a really simple example. The problem here is that, for every await, the method is put in the event loop. That is OK, when you understand it, but this does not prevent your application from calling again the method befor it has retuned a value.

Consider if the method is managing data that is common to the instance of the class and not to the method itself.

So, I have tried the following solution:

bool isWorking = false;

Future<int> foo(int value) async {
if (isWorking) return foo(value);
isWorking = true;

await foo2();
await foo3();
await foo4();
int ret = foo5(value);

isWorking = False;

return ret;
}

As far as I have understood, calling a future method put it immediately in the event loop, so I thought that the execution of the concurrent call of the method was delayed until the first one was ended. But it is not like that, the program enters in an endless loop.

Anywone can give me an explanation and a solution to this question?

Edit: in general I think that it could be interesting to have, like in other languages, a synchronized keyword, with the meaning that the method, if called a second time will wait until the first has ended. Something like:

Future<int> foo(int value) async synchronized {

Edit 2:

I'm really excited because I think I got the solution for this problem that I had for a long time. Thanks to Argenti and in particular to Alexandre that give me the solution. I have simply restructured the solution for easy reuse (at least for me) and I post it here the class I have created and an example on how to use it for those who could need it (try at your own risk ;-) ). I have used a mixin because I find it practical, but you can use the Locker class alone if you like.

myClass extends Object with LockManager {

  Locker locker = LockManager.getLocker();

  Future<int> foo(int value) async {

   _recall() {
      return foo(value);
   } 

   if (locker.locked) {
     return await locker.waitLock();
   }
   locker.setFunction(_recall);
   locker.lock();

   await foo2();
   await foo3();
   await foo4();
   int ret = foo5(value);

   locker.unlock();

   return ret;
  }
}

The class is:

import 'dart:async';

class LockManager {

static Locker getLocker() => new Locker();

}

class Locker {

  Future<Null> _isWorking = null;
  Completer<Null> completer;
  Function _function;
  bool get locked => _isWorking != null;

  lock() {
    completer = new Completer();
    _isWorking = completer.future;
  }

  unlock() {
    completer.complete();
    _isWorking = null;
  }

  waitLock() async {
      await _isWorking;
      return _function();
  }

  setFunction(Function fun) {
    if (_function == null) _function = fun;
  }

}

I have structured the code this way so that you can use it easily in more than one method inside your classes. In this case you need a Locker instance per method. I hope that it can be useful.

J F
  • 1,058
  • 2
  • 10
  • 21

5 Answers5

34

Instead of a boolean you can use a Future and a Completer to achieve what you want:

Future<Null> isWorking = null;

Future<int> foo(int value) async {
  if (isWorking != null) {
    await isWorking; // wait for future complete
    return foo(value);
  }

  // lock
  var completer = new Completer<Null>();
  isWorking = completer.future;

  await foo2();
  await foo3();
  await foo4();
  int ret = foo5(value);

  // unlock
  completer.complete();
  isWorking = null;

  return ret;
}

The first time the method is call isWorking is null, doesn't enter the if section and create isWorking as a Future that will be complete at the end of the method. If an other call is done to foo before the first call has complete the Future isWorking, this call enter the if section and it waits for the Future isWorking to complete. This is the same for all calls that could be done before the completion of the first call. Once the first call has complete (and isWorking is set to null) the awaiting calls are notified they will call again foo. One of them will be entering foo as the first call and the same workflow will be done.

See https://dartpad.dartlang.org/dceafcb4e6349acf770b67c0e816e9a7 to better see the workflow.

Alexandre Ardhuin
  • 71,959
  • 15
  • 151
  • 132
  • Please consider that in my real environment I can have different return values depending on the parameters I pass to the method. But, in general, I did not understood how you code works, more in detail the is Working definition and the await isWorking statement. May you please explain better? – J F Feb 07 '17 at 08:37
  • The snippet doesn't return the same value for every calls. I don't understand your first sentence. – Alexandre Ardhuin Feb 07 '17 at 08:55
  • Very interesting. I've seen some packages that do the same job in a more structured way, but your solution is simple and clean. – J F Feb 07 '17 at 09:15
  • If we have multiple calls to foo at the same time (by different callers), they can't check that isWorking is null and get a lock (race condition)? – Tiago Jul 24 '20 at 23:19
  • Dart being single threaded such a scenario can't happen. – Alexandre Ardhuin Jul 27 '20 at 09:18
  • There is no need to explicitly initialize variables to null in Dart: `Future isWorking;` – Stewie Griffin May 26 '21 at 12:04
10

It is now 2021, we can use synchronized 3.0.0

var lock = new Lock();
Future<int> foo(int value) async {
    int ret;
    await lock.synchronized(() async {
        await foo2();
        await foo3();
        await foo4();
        ret = foo5(value);
    }
    return ret;
}
s k
  • 4,342
  • 3
  • 42
  • 61
9

The answers are fine, here's just one more implementation of a "mutex" that prevents async operations from being interleaved.

class AsyncMutex {
  Future _next = new Future.value(null);
  /// Request [operation] to be run exclusively.
  ///
  /// Waits for all previously requested operations to complete,
  /// then runs the operation and completes the returned future with the
  /// result.
  Future<T> run<T>(Future<T> operation()) {
    var completer = new Completer<T>();
    _next.whenComplete(() {
      completer.complete(new Future<T>.sync(operation));
    });
    return _next = completer.future;
  }
}

It doesn't have many features, but it's short and hopefully understandable.

lrn
  • 64,680
  • 7
  • 105
  • 121
3

I guess what is really needed is a Dart library that implements concurrency management primitives such as locks, mutexs and semaphores.

I recently used the Pool package to effectively implement a mutex to prevent 'concurrent' access to resource. (This was 'throw away' code, so please don't take it as a high quality solution.)

Simplifying the example slightly:

final Pool pool = new Pool(1, timeout: new Duration(...));

Future<Null> foo(thing, ...) async {
  PoolResource rp = await pool.request();

  await foo1();
  await foo2();
  ...

  rp.release();
}

Requesting the resource from the pool before calling the async functions inside foo() ensures that when multiple concurrent calls too foo() are made, calls to foo1() and foo2() don't get 'interleaved' improperly.

Edit:

There appear to be several packages that address providing mutexes: https://www.google.com/search?q=dart+pub+mutex.

Argenti Apparatus
  • 3,857
  • 2
  • 25
  • 35
  • Well, this is what I was looking for, I guess. I will give a try to the packages you suggested. Just one question, what does happen one the second call, the method gets freezed until the first one ends? – J F Feb 07 '17 at 08:39
  • Yes, it is blocked waiting for the future returned by `pool.request()` to complete, which does not happen until `rp.release()` is called in a previous call to `foo()` (or if there was no previous call to `foo()`). – Argenti Apparatus Feb 07 '17 at 14:39
  • 2
    I'm actually surprised that the async SDK library (or it's Pub counterpart library) does not include a mutex class of some sort . – Argenti Apparatus Feb 07 '17 at 14:42
  • 1
    I've also used the pool package with a pool of 1. The issue I had is that it was not re-entrant so it could lead to some dead-lock. That is why i wrote the synchronized package. It uses Completer as Alexandre Ardhuin proposed above and Zones to be re-entrant with a simple api (inspired from java synchonized api) and no dependencies – alextk Feb 12 '17 at 11:41
3

Async and concurrency are two separate topics. There's nothing concurrent about the code above, it all executes serially. The code you have continues to insert more foo(value) events while isWorking is true - it'll never complete.

The tool you're looking for is a Completer<T>. The particular code snippet in question is hard to discern, so I'll give another example. Let's say you have code that is sharing a database connection. Opening a database connection is an asynchronous operation. When one method asks for a database connection, it awaits for the open to complete. During that waiting, another method asks for a database connection. The desired result is that only one database connection gets opened and that one database connection is returned to both callers:

Connection connection;
Completer<Connection> connectionCompleter;
bool get isConnecting => connectionCompleter != null;

Future<Connection> getDatabaseConnection() {
  if (isConnecting) {
    return connectionCompleter.future;
  } 

  connectionCompleter = new Completer<Connection();

  connect().then((c) {
    connection = c;
    connectionCompleter.complete(c);
    connectionCompleter = null;
  });

  return connectionCompleter.future;
}

The first time a method invokes getDatabaseConnection, the connect function is called and the completer's future is returned to the caller. Assuming getDatabaseConnection is invoked again prior to connect completing, the same completer's future is returned, but connect is not called again.

When connect finishes, the completer's event is added to the event queue. This triggers the two futures returned from getDatabaseConnection to complete - in order - and connection will be valid.

Notice that getDatabaseConnection isn't even an asynchronous method, it just returns a Future. It could be asynchronous, but it'd just waste CPU cycles and make your call stack ugly.

Joe Conway
  • 1,566
  • 9
  • 8
  • Thanks Joe, but your code does not work for me, because the function I use in real environment returns different values depending on different parameters. In your case subsequent calls of the function would return data based on the selection of the first call. I need something to pause calls while the one that is running ends. – J F Feb 07 '17 at 08:32