常常在系統裡面都會有一些工作執行起來特別花時間,比如說寄送e-mail,或是寫入複雜資料到資料庫中等等。在一般系統中如果沒有特殊處理的話,會造成使用者在使用的時候感覺很慢才有回應,而回應其實只是一個簡單的執行完成的確認。
這時候非同步處理這個好用的技巧就派上用場了,如果我們 能夠讓使用者執行這個動作的時候,不用真的等到他完成再return回去給使用者,其實只要被呼叫的程式確認已經收到了指令跟參數,就可以回應了,而實際 上執行工作的任務就交給另一個thread來處理即可。
要怎麼做到這點呢?筆者過去看過了不少有趣的解決方式,常看到一些資料庫的愛用者的作法,就是開一個table,然後把一筆一筆要執行的指令內容塞到這個table,然後由另一個thread來定期掃這個table未執行過的指令, 找到還沒執行的指令就把他執行。這也是一種解決方式啦,只是感覺為了執行指令還要維護一個table麻煩了點,而隨時去掃瞄table效能也差了一點。
比較聰明一點的開發者就會想了,那我們在記憶體裡面作一個queue來記錄包成物件的要執行的指令就好啦,就不必寫在資料庫裡面。但是執行方式也是一樣,我們開一個thread每一定時間執行一次來掃這個queue執行還沒執行過的工作。這樣效能是不差,但是還是沒辦法達到趨近於及時的效果,也就是說至少都還是得等一定時間系統才會來執行這樣的工作。
其實有一個更簡單的解法,這樣的queue通常根本不必自己寫,有一種東西叫做 Blocking Queue,也就是說他是一個會把thread lock住的一個queue,什麼時候會lock住呢?就是在這個queue東西被清光的時候。
也就是說,這個queue的使用方式很簡單,我們要執行的工作包成物件跟之前一樣就把他丟到這個Blocking Queue裡面,而另外我們起一個thread在一個迴圈裡面不斷的從Blocking Queue中取得下一個要執行的工作,如果queue裡面已經沒有工作可以給這個thread作了,這個thread就會被queue停住,直到有下一個工作丟入queue中,才會把thread叫醒,繼續執行工作。Java文件中有的簡單用法大概如下:
class Producer implements Runnable {
private final BlockingQueue queue;
Producer(BlockingQueue q) { queue = q; }
public void run() {
try {
while(true) { queue.put(produce()); }
} catch (InterruptedException ex) { ... handle ...}
}
Object produce() { ... }
}
class Consumer implements Runnable {
private final BlockingQueue queue;
Consumer(BlockingQueue q) { queue = q; }
public void run() {
try {
while(true) { consume(queue.take()); }
} catch (InterruptedException ex) { ... handle ...}
}
void consume(Object x) { ... }
}
class Setup {
void main() {
BlockingQueue q = new SomeQueueImplementation();
Producer p = new Producer(q);
Consumer c1 = new Consumer(q);
Consumer c2 = new Consumer(q);
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
}
這樣的blocking queue其實自己實做起來還算滿簡單的,筆者當初想到這種解法的時候便自己implement了一個Blocking Queue來達到這樣目的,不過一陣子後就發現Java 1.5以後,standard library裡面就給了一個Blocking Queue來給你使用。就更省事更不會出錯啦!