【问题标题】:How to make an asynchronous self-calling loop non-recursive如何使异步自调用循环非递归
【发布时间】:2014-08-19 03:56:07
【问题描述】:

我正在用 PHP 编写一个循环遍历数组的函数,然后对其执行异步调用(使用 Promise)。

问题在于,使这个循环发生的唯一方法是让函数异步调用自身。我很快就遇到了 100 个嵌套函数的问题,我基本上想把它改成不再出现。

function myloop($data, $index = 0) {

    if (!isset($data[$index])) {
        return;
    }

    $currentItem = $data[$index];
    $currentItem()->then(function() use ($data, $index) {
       myloop($data, $index + 1);   
    });

}

对于那些想从实际角度回答这个问题的人(例如:重写为非异步),我正在尝试函数式和异步模式,我想知道是否可以使用 PHP 来做到这一点。

【问题讨论】:

  • 在这种情况下使用生成器不是更好吗?你是如何真正实现你的承诺的?我假设您有一个通过将回调转储到堆栈上来实现 then() 的 Promise 类?然后触发 resolve() 方法?
  • @MarkBaker:如果使用生成器更好,我很想知道!不确定将回调转储到堆栈上是什么意思,但我的承诺课程在这里:github.com/fruux/sabre-event/blob/master/lib/Promise.php
  • P.S.在我的实际用例中,$data 是一个生成器,但我从示例中删除了它,因为我觉得它使事情变得更加复杂。
  • @MarkBaker:我明白你的问题是什么意思。如果 promise 已经解决,则 then() 会立即被调用。仅当循环中的所有承诺都已解决时,才会出现问题。
  • 你应该从then返回myloop调用,然后也从!isset返回,一个promise代表一个值。此外,已经解决的承诺立即执行.then 是非常糟糕的 - 切换实现。

标签: php asynchronous functional-programming promise


【解决方案1】:

我已经用伪代码编写了一个可能的解决方案。这个想法是限制 使用数据库一次异步运行的项目数 队列。 myloop() 不再直接递归,而是被调用 每当一个项目完成运行。在示例数据中,我对其进行了限制 同时到4个项目(任意值)。 基本上,它仍然在递归调用自己,但是以一种迂回的方式, 避免你提到的许多嵌套调用的情况。

执行流程:

myloop() ---> queue
^              v
|              |
'<-processor <-'  

<?php    
//----------
// database

//table:  config
//columns: setting, value
//items:    ACTIVE_COUNT, 0
//          ITEM_CONCURRENT_MAX, 4
//table: queue
//columns: id, item, data, index, pid, status(waiting, running, finished), locked
//  --- end pseudo-schema ---

<?php    
// ---------------
// itemloop.php
// ---------------

//sends an item and associated data produced by myloop() into a database queue,
//to be processed (run asynchronous, but limited to how many can run at once) 
function send_item_to_processor($item, $data, $index, $counter) { 
   //INSERT $item to a queue table, along with $data, $index (if needed), $counter, locked = 0
   //status == waiting
}

//original code, slightly modified to remove direct recursion and implement 
//the queue.
function myloop($data, $index = 0, $counter = 0) {

    if (!isset($data[$index])) {
        return;
    }

    $currentItem = $data[$index];
    $currentItem()->then(function() use ($data, $index) {
       //instead of directly calling `myloop()`, push item to
       //database and let the processor worry about it. see below.
       //*if you wanted currentItem to call a specific function after finishing,
       //you could create an array of numbered functions and pass the function
       //number along with the other data.* 
       send_item_to_processor($currentItem, $data, $index + 1, $counter + 1);
    });

}


// ---------------
// processor.php
// ---------------

//handles the actual running of items.  looks for a "waiting" item and 
//executes it, updating various statuses along the way. 
//*called from `process_queue()`*
function process_new_items() { 
   //select ACTIVE_COUNT, ITEM_CONCURRENT_MAX
   //ITEM_COUNT = total records in the queue. this is done to
   //short-circuit the execution of `process_queue()` whenever possible
   //(which is called frequently).
   if (ITEM_COUNT == 0 || $ACTIVE_COUNT >= $ITEM_CONCURRENT_MAX) 
     return FALSE;
   //select item from queue where status = waiting AND locked = 0 limit 1;
   //update item set status = running, pid = programPID
   //update config ACTIVE_COUNT = +1
   //**** asynchronous run item here ****//
   return TRUE;
}  

//main processor for the queue.  first processes new/waiting items
//if it can (if too many items aren't already running), then processes
//dead/completed items.  Upon an item.status == finished, `myloop()` is
//called from this function.  Still technically a recursive call, but 
//avoids out-of-control situations due to the asynchronous nature.    
//this function could be called on a timer of some sort, such as a cronjob
function process_queue() {
  if (!process_new_items()) 
    return FALSE;  //too many instances running, no need to process

  //check queue table for items with status == finished or is_pid_valid(pid) == FALSE
  $numComplete = count($rows);
  //update all rows to locked = 1, in case process_queue() gets called again before
  //we finish, resulting in an item potentially being processed as dead twice. 
  foreach($rows as $item) {
    if (is_invalid(pid) || $status == finished)  { 
       //and here is the call back to myloop(), avoiding a strictly recursive 
       //function call.  
       //*Not sure what to do with `$item` here -- might be passed back to `myloop()`?.*
       //delete item(s) from queue
       myloop(data, index, counter - 1);
       //decrease config.ACTIVE_COUNT by $numComplete
    }
  }
}

【讨论】:

  • 嗨@Trick,我主要想知道理论上是否可以将我的函数重写为不使用递归的函数。使用第二个函数(循环函数)确实让这变得超级简单,但我想避免这种情况。
猜你喜欢
  • 2015-03-14
  • 2017-03-12
  • 2015-06-09
  • 2017-10-28
  • 2015-08-27
  • 1970-01-01
  • 2018-01-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多