Master-Worker Pattern

Master-Worker Pattern

Master-Worker是一個很好用的設計模式,
它可以把一個大工作分成很多小工作去執行, 等所有小工作都回來以後,
將所有的小工作結果做一個統整, 得到最終結果,
就很像專題小組有五個人, 把系統分成四等分, 每個人負責一份,
最後那個人負責統整, 會比只有一個人處理整個系統還要有效率。

從上圖可以得知, master是負責協調任務跟整合結果的中繼者,
所以我們試著寫出該設計模式。

題目: 在一個很大的字串陣列內, 找出那些字串含有"1"的元素。

解法: 先將每個陣列元素視為一個Worker處理的task, 再透過每個Worker分工找出字串, 
      結果回傳至結果區, 繼續找下一個task, 直到所有task都結束。

master程式碼

public class Master {
    protected Queue<Object> mWorkQueue;
    protected List<Thread> mThreadList;
    protected Map<String, Object> mResultMap;

    public boolean isComplete() {
        for (Thread t : mThreadList) {
            if (t.getState() != Thread.State.TERMINATED) {
                return false;
            }
        }
        return true;
    }

    public Master(Worker worker, int countWorker) {
        mWorkQueue = new ConcurrentLinkedQueue<Object>();
        mResultMap = new ConcurrentHashMap<String, Object>();
        mThreadList = new ArrayList<Thread>();

        worker.setWorkQueue(mWorkQueue);
        worker.setResultMap(mResultMap);
        for (int i = 0; i < countWorker; i++) {
            mThreadList.add(new Thread(worker, Integer.toString(i)));
        }
    }

    public void addTask(Object job) {
        mWorkQueue.add(job);
    }

    public Map<String, Object> getResult() {
        return mResultMap;
    }

    public void execute() {
        for (Thread t : mThreadList) {
            t.start();
        }
    }
}

建構子把一個Worker丟進來, 包成一個Thread放進Thread Array。

宣告一個Queue, 所有底下的工作者, 都會存取這個Queue, 由於一次只能有一個worker取出, 避免race conditiion,
因此宣告成Concurrent, 又由於先進先出的特性, 因此會宣告成Queue。

再來宣告一個一個Map, 理由同上, 由於所有工作者都會把計算好的結果丟進Map, 因此只允許一次一個Worker進行存取,
所以宣告成Concurrent, Map是用來存放所有結果的區域。

那我們會將每個要找的字串(或物件)透過addTask存放到Queue內, 等到所有工作完成, 則可以回傳Map,
因此會多宣告一個方法來進行判斷是否全部task都完成了。

Worker程式碼

public abstract class Worker implements Runnable{
    protected Queue<Object> mWorkQueue;
    protected Map<String, Object> mResultMap;

    public void setWorkQueue(Queue<Object> workQueue) {
        this.mWorkQueue = workQueue;
    }

    public void setResultMap(Map<String, Object> resultMap) {
        this.mResultMap = resultMap;
    }

    //subclass handle task
    public abstract Object handle(Object task);

    @Override
    public void run() {
        while (true) {
            Object input = mWorkQueue.poll();
            if (input == null) {
                break;
            }
            Object re = handle(input);
            mResultMap.put(Integer.toString(input.hashCode()), re);
        }
    }
}

Worker就是用來處理每個小任務的人, 一開始會有兩個方法, 一個是取得Queue, 這樣才能拿到每一個task,
另外一個方法就是取得Map, 這樣才可以把算完的結果丟回去,
那至於run就是我們要從Queue取出任務, 執行完再塞回Map的部分,
至於handle方法不知道, 因為這是一個虛擬類別, 所以真正的worker才會知道要處理怎樣的任務,
因此我們必須繼承這個類別, 並且覆寫handle這個方法。

Task的程式碼

public class MyString{
    private String str;

    private boolean isFind;

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
    }

    public boolean isFind() {
        return isFind;
    }

    public void setIsFind(boolean isFind) {
        this.isFind = isFind;
    }
}

我們任務裝的物件很簡單, 就是一個字串, 以及這個結果是否有被找到。

再來就是我們實際Worker的程式碼了。

public class StringWorker extends Worker {

    private String mSearchText;

    public StringWorker(String searchText){
        mSearchText = searchText;
    }

    @Override
    public Object handle(Object task) {
        MyString strObj = new MyString();
        if(task == null){
            return strObj;
        }
        if(task instanceof MyString){
            strObj = (MyString) task;
            String s = strObj.getStr();
            if(s.indexOf(mSearchText) != -1){
                strObj.setIsFind(true);
            } else{
                strObj.setIsFind(false);
            }
        }
        return strObj;
    }
}

他覆寫了Worker類別, 在handle方法內, 將task拆解開來, 從建構子傳入要尋找的字串進行比對,
將結果塞入task的物件並且回傳。

主程式程式碼

public class Main {
    public static void main(String[] args){
        Master master = new Master(new StringWorker("1"), 5);
        for (int i = 0; i < 20; i++) {
            MyString ms = new MyString();
            ms.setStr(String.valueOf(i));
            ms.setIsFind(false);
            master.addTask(ms);
        }
        master.execute();

        while (!master.isComplete()) ;
        Map<String, Object> resultMap = master.getResult();
        for(String key : resultMap.keySet()){
            MyString ms = ((MyString) resultMap.get(key));
            System.out.println("name:" + ms.getStr());
            System.out.println("is find:" + ms.isFind());
        }
    }
}

在此先做一個小範圍的實驗, 即20筆資料即可, 利用Master建構子宣告出5個Worker,
並且希望工作者找出task內是否有”1”字串存在,
在for迴圈內, 是用來初始化所有的工作, 以index做為資料的內容,
最後將每個task透過master加入到Queue內部, 最後執行master內所有的Worker。

在我們所有任務沒有完成之前, 會讓master一直空轉在while迴圈上面,
如果是在Android建議將此部分放入到Thread, 避免user無法進行操作。

等到所有任務完成, 則把每個Task的name跟isFind都印出。

name:8
is find:false
name:13
is find:true
name:2
is find:false
name:15
is find:true
name:12
is find:true
name:1
is find:true
name:18
is find:true
name:14
is find:true
name:10
is find:true
name:5
is find:false
name:9
is find:false
name:0
is find:false
name:11
is find:true
name:3
is find:false
name:17
is find:true
name:4
is find:false
name:7
is find:false
name:19
is find:true

你會看到每個任務完成的時間不一定, 所以每次執行的結果順序也都不一致。
以上就是有效率的Master-Worker Pattern。
程式碼
github