执行前检查异步任务依赖关系的设计模式

Design pattern for checking asynchronous task dependencies before execution

问题

给定一些异步加载的依赖项,我想在所有依赖项加载完成后才触发一些代码。作为一个简单的例子,请考虑以下伪代码:

bool firstLoaded = false, secondLoaded = false, thirdLoaded = false;

function loadResourceOne() {
    // Asynchronously, or in a new thread:
    HTTPDownload("one.txt");
    firstLoaded = true;
    if (secondLoaded && thirdLoaded) {
        allLoaded();
    }
}

function loadResourceTwo() {
    // Asynchronously, or in a new thread:
    HTTPDownload("two.txt");
    secondLoaded = true;
    if (firstLoaded && thirdLoaded) {
        allLoaded();
    }
}

function loadResourceThree() {
    // Asynchronously, or in a new thread:
    HTTPDownload("three.txt");
    thirdLoaded = true;
    if (firstLoaded && secondLoaded) {
        allLoaded();
    }
}

function allLoaded() {
    Log("Done!");
}

/* async */ loadResourceOne();
/* async */ loadResourceTwo();
/* async */ loadResourceThree();

我在找什么

这是一个我发现自己不得不在不同的语言和不同的环境中反复解决的问题。然而,每次我发现自己使用该语言提供的工具来拼凑一些简单的解决方案,比如在 JavaScript 中将每个异步资源作为 Promise 返回,然后使用 Promise.all() —— 或者单独加载每个资源在 Python 线程然后使用 threads.join()

我正在尝试找到一种设计模式 来解决一般情况下的这个问题。最佳解决方案应满足两个条件:

  1. 可以应用于任何支持异步操作的语言
  2. 最大限度地减少代码重复(请注意,在我的简单示例中,行 allLoaded(); 重复了三次,它前面的 if 语句实际上 重复,如果我需要第四个或第五个依赖项,则无法很好地扩展)
  3. 当所有资源都加载完毕后,尽快运行最后的回调——这很明显,但像"check that all are loaded every 5 seconds"这样的解决方案是不可接受的

我试着翻阅“四人帮”设计模式的索引,但跳出来的几个模式名称可能是线索,但结果却无关紧要。

如何使用依赖项数量初始化锁存器,并且每次加载程序完成时,单独的加载器都会递减它。

一旦锁存器计数=0,就这样;我们知道所有都已加载并且可以触发回调/所需函数。

对于Java - https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/CountDownLatch.html

您正在寻找 Fork-Join pattern

In parallel computing, the fork–join model is a way of setting up and executing parallel programs, such that execution branches off in parallel at designated points in the program, to "join" (merge) at a subsequent point and resume sequential execution. Parallel sections may fork recursively until a certain task granularity is reached. Fork–join can be considered a parallel design pattern...

实施将取决于语言,但您可以结合您选择的语言搜索 fork-join。请注意,您不会在四人组中找到异步模式。您可能需要一本专门针对多线程或并行计算的书。

I tried flipping through the index of the Gang of Four's Design Patterns, but the few pattern names that jumped out at me as possible leads turned out to be unrelated.

这个问题领域需要结合多种设计模式,而不是单一的设计模式。让我们解决关键要求:

  1. 任务应该能够知道它所依赖的任务何时完成 以便它可以立即开始执行。这需要在没有 定期轮询相关任务。
  2. 需要能够向任务添加新的依赖项,而无需继续添加新的 if-else 样式检查。

对于第 1 点,我建议您看一下 Observer pattern。在您的情况下,这种模式的主要优点是任务不必轮询它的依赖任务。相反,您的任务所依赖的每个任务都会在任务完成时通过调用 update 方法通知您的任务。 update 方法可以智能地实现,以在每次调用该方法时检查它所依赖的预先填充的任务列表。当所有预先配置的任务列表都调用 update 时,任务可以启动它的工作程序(例如线程)。

关于第2点,我建议你看看Composite patternTask 有一个 array 个从属 Task 个实例和一个 arrayTask 个它依赖的实例。如果任务完成执行,它会在依赖于它的任务数组中的每个任务上调用 update。另一方面,对于开始执行的任务,它所依赖的其他任务将调用它的 update 方法。

如果我必须在伪代码中定义上述方法,它将如下所示:

Task structure :
   array of dependents : [dependent Task instances that depend on this Task]
   array of dependencies : [Task instances this task depends on]

   function update(Task t) : 
       remove t from dependencies
       if(dependencies size == 0) 
          - start asynchronous activity (call executeAsynchronous)

    function executeAsynchronous() :
        - perform asynchronous work
        - on completion :
             - iterate through dependent array
               - call update on each Task in dependent array and pass it this Task

    function addDependent(Task t) :
       - add t to array of dependent tasks

    function addDependency(Task t) :
       - add t to array of dependencies 

总而言之,不要去寻找设计模式来解决您的问题。相反,提出工作代码并通过它来改进它的设计。


注意:框架和设计模式之间存在细微但重要的区别。如果 objective 是使用设计模式构建任务依赖框架,那么您肯定需要不止一种设计模式。上面的答案解释了如何使用四人帮模式来做到这一点。如果 objective 是 不重新发明 轮子,那么可以看看已经解决了这个问题的框架。

一个这样的框架是 Spring Batch 框架,它允许您定义 sequential flows and split flows which can be wired together into a job 定义端到端处理流程。