Java Concurrency: Producer and Consumer,生產者消費者模式,解耦合的極致
如何優化流程,到處都在使用的Producer and Consumer模式,此篇文章讓你五分鐘就理解
前言
在介紹過Thread-Per-Message和Worker Thread後,最基本的Concurrency開發方法就算可以掌握了,接下這篇文章介紹進階的Concurrency Pattern,Producer and Consumer,Producer and Consumer(後文以P&C表示)模式到處可見,像是網路I/O多路復用,Message Queue的使用,大多都是P&C的程式實踐。
Producer and Consumer
P&C用通俗易懂的生活舉例,相信大家都在早餐店都買過老闆已經做好並且放在櫃檯或小籃子裡的的三明治,早上匆忙的時候都會直接購買避免點餐現做的等待,我們來分析一下這個過程。
- 老闆做三明治 (生產者Producer生產產品)
- 放到籃子裡
- 客人拿走付錢 (消費者Consumer拿走產品)
而這個流程會延伸兩種情形
- 籃子有三明治 (下一個客人可以直接拿取) — get
- 籃子沒三明治 (客人如果要購買需要等待老闆現做) — wait
這例子也就是P&C模式,目的就是為了解耦原本需要等待的行為(點餐等老闆做轉變為老闆有空就做客人看到就拿)
實際應用
在這章節會演示一個實際範例功能,並且從Thread-Per-Message到Worker Thread再到Producer and Consumer三種不同角度思考。
假設系統有個使用者購買商品的功能,但是此商品比較特別與珍貴,所以每次下訂成功都需要將訂單紀錄做成報表提交給公司內商品稽核系統。而系統流程如下
- 系統接收到請求 (0.5秒)
- 紀錄訂單到系統的DB (0.5秒)
- 產報表並且寄送到監管系統 (3秒)
這樣的功能只透過單執行序想必一定是不可行的,因為整個過程需要4秒的時間,所以一定需要開執行序來執行(在平常使用Tomcat此類的Web Container就已經幫開發者處理好了)
Thread Per Message
上方式Thread Per Message的範例圖,之前Thread Per Message的文章有提過,此模式非常好開發Concurrency的功能,但是若Task(連線要求)數量增多,會導致產製的Thread過多,CPU需要瘋狂的切換不同Thread和產製銷毀Thread以至於效率反而不佳。所以可以在藉由Worker Thread來限制Thread的數量(需透過數據或是經驗取一個Thread數量平衡)
Worker Thread
我們使用了Worker Thread來避免過多創建Thread執行任務的問題,但是會造成超過Worker Thread負荷的任務需要等待其他任務完成才能被處理。
假設Worker Thread設定為二(每次最多兩個Thread執行),那如果同一時間有三個任務進到系統,第三個任務需要等待8秒的時間使用者才會收到返回的結果(如圖)。
使用Worker Thread會導致Request卡住但是不使用又會導致CPU瘋狂切換(Context Switch)以及創建銷毀Thread,這時候就是Producer Consumer模式登場的好時機,這功能如果需求量非常大那一定不可能考慮Per Message,一定也是以Worker Thread開發,但可以根據流程觀察到,使用者其實不需要知道資料有產製報表而且送到監管系統,我們只需要確認寫到DB告訴他交易成功就好。這樣就可以將功能切成兩個部分,
- 接收請求並且存到DB
- 產製報表寄送到監管系統
這兩部分就可以嘗試解偶,讓第一個行為成為Producer創造要執行第二個行為的任務,並且使用Queue的方式存放,而第二個行為成為Consumer當Queue有任務時就執行,沒任務就等待,這如同前言的早餐店客人的範例。
這樣使用者只需等待一秒就可以得到結果,後面的行為交由另一組Worker Thread執行,比一開始需要等待4秒提升更多的使用者體驗。
範例程式碼
此範例程式碼,直接使用Java提供的線程池當作Worker Thread Pool,如果想看原生實作可以閱讀Concurrency系列文中的Worker Thread文章。程是碼有三個Producer與兩個Consumer,並且使用了Sleep模擬任務執行消耗秒數,
結論
總結實際應用變成P&C模式的思路
- 單執行序執行,其他使用者都會被卡住等待四秒
- 使用Thread-Per Message,如果請求過多會導致CPU Context Switch與創建銷毀Thread成本開銷(記憶體)
- 使用Worker Thread,可以避免Thread Per Message的缺點,但是超過數量後會如同單執行序執行,超過的請求需要卡住四秒
- 梳理系統流程,引入Producer Consumer解耦系統邏輯,讓使用者可以等待一秒就拿到結果,但是系統依然可以執行後續的任務
上面的演化最關鍵的還是籃子(早餐店籃子),在Java可以使用Blocking Collection(BlockingQueue)的實作當作Worker Thread的任務Queue避免Race Condition。
Producer and Consumer,實踐並不會太困難,他也不是一定需要使用,但是如果遇到了
- 流量消峰 (避免大流量導致系統機器負擔不來)
- 流程解耦合 (如同實際應用範例)
就可以嘗試使用。