我非常喜欢Dart中的async / await模式。 它允许我编写可读的方法。
但是,有一些事情是有问题的,尤其是一些事情,我根本不知道管理的热情。
问题是在方法中使用异步和多次等待,我们在方法中引入了并发性。 例如,如果我有一个方法:
Future<int> foo(int value) async {
await foo2();
await foo3();
await foo4();
int ret = foo5(value);
return ret;
}
嗯,这是一个非常简单的例子。 这里的问题是,对于每个await,该方法都放在事件循环中。 没关系,当你理解它时,但这并不妨碍你的应用程序再次调用方法,因为它已经重新调整了一个值。
考虑该方法是否正在管理类实例所共有的数据,而不是方法本身。
所以,我尝试了以下解决方案:
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;
}
据我所知,调用future方法会立即将它放在事件循环中,所以我认为方法的并发调用的执行被推迟到第一个结束时。 但不是那样,程序进入无限循环。
任何人都可以给我一个解释和解决这个问题的方法吗?
编辑: 总的来说,我认为像其他语言一样,拥有一个synchronized关键字,意味着该方法,如果第二次调用将等到第一次结束,这可能会很有趣。 类似的东西:
Future<int> foo(int value) async synchronized {
编辑2:
我真的很兴奋,因为我觉得我已经解决了这个问题,我已经有很长一段时间了。感谢Argenti,尤其是Alexandre给我的解决方案。我只是简单地重新构建了解决方案以便于重用(至少对我来说),我在这里发布了我创建的类,以及如何将它用于可能需要它的人的示例(尝试自担风险;-))。 我使用了mixin,因为我发现它很实用,但如果你愿意,可以单独使用Locker类。
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;
}
}
课程是:
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;
}
}
我已经以这种方式构建了代码,以便您可以在类中的多个方法中轻松使用它。在这种情况下,每个方法需要一个Locker实例。 我希望它有用。
答案 0 :(得分:4)
答案很好,这里只是&#34; mutex&#34;的另一个实现。这可以防止异步操作被交错。
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;
}
}
它没有很多功能,但它很简短,希望可以理解。
答案 1 :(得分:3)
您可以使用Future
和Completer代替布尔值来实现您的目标:
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;
}
第一次调用isWorking
方法为null
时,不会进入if
部分并创建isWorking
作为将在最后完成的未来方法。如果在第一次通话完成未来foo
之前对isWorking
进行了其他通话,则此通话会进入if
部分,并等待未来isWorking
完成。对于在第一次呼叫完成之前可以完成的所有呼叫,这是相同的。第一次通话完成后(isWorking
设置为null
),等待来电通知他们将再次拨打foo
。其中一个将作为第一个电话进入foo
,并且将完成相同的工作流程。
请参阅https://dartpad.dartlang.org/dceafcb4e6349acf770b67c0e816e9a7以更好地查看工作流程。
答案 2 :(得分:2)
我想真正需要的是一个实现并发管理原语的Dart库,例如锁,互斥锁和信号量。
我最近使用Pool程序包有效地实现了一个互斥锁,以防止&#39;并发&#39;访问资源。 (这是&#39;扔掉代码,所以请不要把它作为高质量的解决方案。)
稍微简化示例:
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();
}
在调用foo()中的异步函数之前从池中请求资源可以确保当多个并发调用太foo()时,调用foo1()和foo2()不会得到&#39;交织&#39;不当。
编辑:
似乎有几个软件包可以解决提供互斥的问题:https://www.google.com/search?q=dart+pub+mutex。
答案 3 :(得分:0)
异步和并发是两个独立的主题。上面的代码没有任何并发,它都是串行执行的。您正在使用的代码继续插入更多foo(value)
事件,而isWorking是真的 - 它永远不会完成。
您正在寻找的工具是Completer<T>
。有问题的特定代码片段难以辨别,因此我将举一个例子。假设您拥有共享数据库连接的代码。打开数据库连接是一种异步操作。当一个方法要求数据库连接时,它等待open打开完成。在等待期间,另一种方法要求数据库连接。所需的结果是只打开一个数据库连接,并且向两个调用者返回一个数据库连接:
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;
}
方法第一次调用getDatabaseConnection
时,调用connect
函数,并将完成者的未来返回给调用者。假设在getDatabaseConnection
完成之前再次调用connect
,将返回相同的完成者的未来,但不会再次调用connect
。
当connect
完成时,完成者的事件将添加到事件队列中。这将触发从getDatabaseConnection
返回的两个期货按顺序完成 - connection
将有效。
请注意,getDatabaseConnection
甚至不是异步方法,只返回Future
。它可能是异步的,但它只会浪费CPU周期并使你的调用堆栈变得丑陋。
答案 4 :(得分:0)
现在是 2021 年,我们可以使用 synchronized 3.0.0