在寫 IRC 機器人的時候一定會碰到要控制輸出訊息量的問題,不能一次爆量傳到 server 上, 不聽話 server 就會把機器人踢掉。目前所用的機器人是以 AnyEvent::IRC 來寫的,因此以 AnyEvent 為基礎,思考了幾個解法。

第一個解法最容易,就是有個定時器,固定每秒送一則訊息,而要送訊息時,則只是把訊息 內容塞到一個佇列中,不真的立刻送出,一個寫起來大致上像這樣:

my $bot = ...; # 假設是個 irc 機器人物件
my @queue;

my $timer = AE::timer 0, 1 sub {
    return unless @queue;
    $bot->send( shift @queue );
};

# 對外的介面
sub send_message {
    push @queue, $_[0];
}

不過這個解法有個缺點,如果這個機器人基本上很閒,偶爾才比較忙一下,那 $timer 物 件會讓機器人每秒鐘做一次白工。如果能讓它「沒訊息時就不要再做白工」可能是比較理想的。

基於這樣的概念,於是第二個解法就出現了:

my $bot = ...;
my @queue;
my $timer;

sub send_message {
    push @queue, $_[0];

    $timer ||= AE::timer 0, 1 sub {
        $bot->send( shift @queue );
        undef $timer unless @queue;
    };
}

稍微改一下名字就變得像是在做 Job Control:

sub work_for_real { ... }
my @job_queue;
my $job_timer;

sub work {
    push @job_queue, $_[0];

    $job_timer ||= AE::timer 0, 1, sub {
        work_for_real( shift @job_queue );

        undef $job_timer unless @job_queue > 0;
    };
}

但還是有個問題,應該說,有個隱性的前提存在:這個工作內容(送出訊息)可以在一秒內 完成。如果不行,甚至超過很多的話,前述的 work_for_real 這個 sub-routine 就會有 重入(reentrant)的現象。 視實做而言,可能因此發生不好的競爭條件、或是死結(等等諸如此類在恐龍書裡面出 現的關鍵字)。

如果假設 work_for_real 本身的內容是同步型式,也就是在裡面基本上沒有留下其他 任 務未完成的 AnyEvent Watcher,那可以改寫成「前一件事做完了,再開始做下一件事」的架 構,看起來類似這樣(注意到 AE::timer 的參數與前例不同):

sub work_for_real { ... }
my @job_queue;
my $job_timer;

sub work_on_next_job {
    return if @job_queue == 0;

    work_for_real( shift @job_queue );

    $job_timer = AE::timer 1, 0, sub {
        undef $job_timer;

        work_on_next_job;
    };
}

sub work {
    push @job_queue, $_[0];
    work_on_next_job() unless $job_timer;
}

最後這種型式也比較有機會動態調整計時器的參數,比方說配合 @job_queue 增加的速度 來調整輸出的速度之類的。