async-awaitdart

Dart: how to manage concurrency in async function


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.


Solution

  • 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.