輪流做事的工人

作者:   發佈於:  

AnyEvent::Worker 是個把工作丟去其他行程執行,又超簡單而通用的的模組。對付「可能會花一段不定時間執行, 但此時並不需要等待它做完」的工作,特別有用。

基本上的使用方式如下:先建出工人物件($secretary),提供個回呼描述一下做事方法,然後在有工作事項時, 調用其 do 方法。

my $secretary = AnyEvent::Worker->new(sub {
    my ($task) = @_;

    # 可能做很久
    get_busy($task);
});

my @tasks = (
    "my dirty laundry",
    "send postcard to wife",
    "drive my daughter back from school"
);

for (@tasks) {
    $secretary->do($_, sub {  ... # DONE })
}

AnyEvent::Worker 會自動分支出一個新的工人行程專門執行回呼中的程式碼。而其參數 (@tasks)是從主行程,透過 socket 傳到工人行程的行程中。因此較建議用一般純量來表 示工作內容,而不要用物件。

上述範列程式中有三件事項要辨,在最後的 for 迴圈中依序丟給工人,此處的 do 會立 刻傳回,不會等到工作做完。使用時需要在之後的程式碼檢查工作是否完成。

而在分支出來工人行程中,這三件事情是一件接著一件做完的,如果目前工作還沒完畢,新 進的工作就會等著。AnyEvent::Worker 沒有提供自動分支出三支工人行程,平行完成事項 的機制。不過,如果工作項目有很多,原則上也不希望分支出爆量的工人行程,瞬間把資源 佔滿。大致上是希望,如果機器有 N 核心 CPU,就分支出 N 支工人,並將工作平均分配下 去。

那麼,要如何做到這點呢?

其實只要用 Data::RoundRobin 就可以簡單做到。這個模組將一列變數變一組, 可以輪流取出使用,取完後再從頭開始。例如:

my $r = Data::RoundRobin->new(qw(a b c));

say $r->next; #=> a
say $r->next; #=> b
say $r->next; #=> c
say $r->next; #=> a
say $r->next; #=> b

正好可以符合「平均分配工作」的概念。

以下建立四個工人,並且分配在同一組中,並將依次將工作分配工作到「下一位」工人:

my $secretaries = Data::RoundRobin->new(
    map { 
        AnyEvent::Worker->new(sub{ ... } )
    } 1..4
);

for my $task (...) {
    $secretaries->next->do($task, sub { ... });
}

這麼一來,就會預先分支出四支工人行程,並依次接下工作。

這樣的組合十分簡單、好寫,但只能符合工作模式單純的情境。如果每項工作所需的時間可 能不定,先分配下去的工作有可能最後被做完,則需要更好的分散機制。以免工作都積在某 一個特定工人手上。另外也可讓工人行程視資源狀況自動增減,而不是一開始就預先分支等 等的需求也是存在的。關於這部份請直接參考如 GearmanTheSchwartz 這類已經非常完整的解法。