1

In Flutter, I have an async function that should not be called twice at the same time.

Here's a minimal example

class DatabaseHelper {

   Future<void> add100elements() async {
      // this functions queries a database and adds 100 elements from the db to a local list
       var elementsToAdd = await db.rawQuery('select * from quotes where id > ? limit 100', this.currentId);
       this.localList.addAll(elementsToAdd);
       this.currentId += 100;
   }

}

There could be two different places in the app that call add100elements at almost the same twice. That means the code will be executed twice, and add the same 100 elements. I want to avoid this, by preventing the add100elements function to be called twice. I could simply do this:

class DatabaseHelper {
   bool isRunning = false;

   Future<void> add100elements() async {
      // this functions queries a database and adds 100 elements from the db to a local list
      if (isRunning) return;
      isRunning = true;

      var elementsToAdd = await db.rawQuery('select * from quotes where id > ? limit 100', this.currentId);
      this.localList.addAll(elementsToAdd);
      this.currentId += 100;
      isRunning = false;
   }

}

But that feels a little manual and clumsy.

Is there a built-in way to do this in dart? Or a better way?

DevShark
  • 8,558
  • 9
  • 32
  • 56
  • Claim and release a semaphore if you want the second invocation to wait until the first completes. If you want the second to fail if the first is in progress your solution looks fine. – Richard Heap Mar 03 '23 at 15:30
  • Does this answer your question? https://stackoverflow.com/questions/42071051/dart-how-to-manage-concurrency-in-async-function – Richard Heap Mar 03 '23 at 15:33
  • Unfortunately no. It is close, but not exactly the same, and, more importantly, dart has evolved so much since 6 years ago that the code is not valid anymore. – DevShark Mar 03 '23 at 15:37
  • "Claim and release a semaphore" : thanks for that, I am happy to look into that. Can you expand? I don't think there is a semaphore class built-in in dart? – DevShark Mar 03 '23 at 15:43
  • 1
    @DevShark Your `bool` is acting as a semaphore. You don't need to anything fancy. – jamesdlin Mar 03 '23 at 15:44
  • Also see https://stackoverflow.com/a/75383147/. – jamesdlin Mar 03 '23 at 15:48
  • https://pub.dev/packages/semaphore_plus (it's just a wrapper around Future/Completer for anyone who prefers the claim/release paradigm) – Richard Heap Mar 03 '23 at 15:49

1 Answers1

2

There have been several valid and useful suggestions in the comments. And I can add a couple things.

Pool

You could consider using the Pool class. This is useful when you want to limit the number of concurrent operations to some value N, but is perhaps overkill when N = 1 as in your use case.

Using Pool would differ from your flag-based approach in that an attempted concurrent run would run after the in-progress operation completes, rather than be skipped.

Flag

Your isRunner flag implementation is just fine, but can be made more robust using try/finally, as in:

   Future<void> add100elements() async {
      // this functions queries a database and adds 100 elements from the db to a local list
      if (isRunning) return;
      isRunning = true;
      
      try {
        var elementsToAdd = await db.rawQuery('select * from quotes where id > ? limit 100', this.currentId);
        this.localList.addAll(elementsToAdd);
        this.currentId += 100;
      }
      finally {
        isRunning = false;
      }
   }

This modification offers the advantage that the flag will be cleared even in the event of an exception. Otherwise, an exception would leave the flag permanently "stuck" in the true state.

You can go one step further if desired and abstract this if it's a common pattern:

import 'dart:async';

class OneAtATime {
  var _isRunning = false;
  
  Future<void> runOrSkip(FutureOr<void> Function() block) async {
    if (_isRunning) return;
    _isRunning = true;
    try {
      await block();
    }
    finally {
      _isRunning = false;
    }
  }
}

OneAtATime runner;

Future<void> add100elements() {
  return runner.runOrSkip(() async {
    var elementsToAdd = await db.rawQuery('select * from quotes where id > ? limit 100', this.currentId);
    this.localList.addAll(elementsToAdd);
    this.currentId += 100;
  });
}
Chuck Batson
  • 2,165
  • 1
  • 17
  • 15