有一段時間沒有更新博客了,最近半年都在着寫書《.NET框架設計—大型企業級框架設計藝術》,很高興這本書將於今年的10月份由圖靈出版社出版,有關本書的具體介紹等書要出版的時候我在另寫一篇文行作介紹。能夠先透露一下,本書是博主多年來對應用框架學習的總結,裏面包含了十幾個重量級框架模式,這些模式都是咱們目前所常用到的,對於學習框架和框架開發來講是很好的參考資料,你們敬請期待。前端
好了,進入文章主題。數據庫
最近幾個月本人一直從事着SOA服務開發工做,簡單點講就是提供服務接口的;從提供前端接口WEBAPI,到提供後端接口WCF\SOAFramework,期間學到了很多有關多線程使用上的經驗,這些經驗有的是本人本身的錯誤使用後的經驗,有些是公司的前輩的指點,總之這些東西你不遇到過你是不會意識到該如何使用的,因此本人以爲頗有必要總結分享給廣大和我同樣工做在一線的博友們。後端
咱們從服務的處理環節爲順序來介紹:服務器
任何服務的調用都須要首先進到服務的入口方法中,該方法一般扮演着領域邏輯的門面接口(將系統用例進行服務接口的劃分),經過該接口進行用例的調用。當咱們須要處理長時間過程時都會面臨着頭疼的超時異常,若是咱們再去設計如何作超時補償措施就會很複雜並且是沒有必要的開銷。長時處理的服務調用場景多半在同步數據中,經過某個JobWs(工做服務)按期的來同步數據(本人就是在這個過程當中學到的),當咱們沒法預知咱們的服務會處理多長時間時,基本上都會首先去設置調用端的鏈接超時時間(是否是都會這麼想?);這很正常,很來超時時間就是用來給咱們用的;可是咱們忽視了咱們當前的業務場景了,若是你的服務不返回任何有關狀態值的話「其實應該開啓一個獨立的線程來處理同步邏輯而讓服務的調用者儘早收到相應」。多線程
1 public class ProductApplicationService 2 { 3 public void SyncProducts() 4 { 5 Task.Factory.StartNew(() => 6 { 7 var productColl = DominModel.Products.GetActivateProducts(); 8 if (!productColl.Any()) return; 9 10 DominModel.Products.WriteProudcts(productColl); 11 }); 12 } 13 }
這樣就能夠儘早解放調用者;經過開啓一的單獨的線程來處理具體的同步邏輯。架構
若是你的服務須要返回某個狀態值怎麼辦?其實咱們能夠參考」異步消息架構模式「來將消息寫入到某個消息隊列中,而後客戶端按期來取或者推送均可以,讓當前的這個服務方法可以平滑的處理,至少爲系統的總體性能瓶頸作了一份貢獻。併發
入口位置一般都會記錄下調用的異常信息,也就是加上一個try{}catch{},用來捕獲本次調用的全部異常信息。(固然你可能會說代碼中充斥着try{}catch{}不是很好,能夠將其放到某個看不見的地方自動處理,這有好有壞,看不見的地方咱們就必然少不了配置,少不了對自定義異常類型的配置,總之事物都有兩面性。)框架
1 public class ProductApplicationService 2 { 3 public void SyncProducts() 4 { 5 try 6 { 7 Task.Factory.StartNew(() => 8 { 9 var productColl = DominModel.Products.GetActivateProducts(); 10 if (!productColl.Any()) return; 11 12 DominModel.Products.WriteProudcts(productColl); 13 }); 14 } 15 catch(Exception exception) 16 { 17 //記錄下來... 18 } 19 } 20 }
像這樣,看上去好像沒問題哦,可是咱們仔細看看就會發現,這個try{}catch{}根本捕獲不到咱們任何異常信息的,由於這個方法是在咱們開啓的線程外面的,也就是說它早就結束了,開啓的線程處理棧中根本就沒有任何的try{}catch{}機制代碼了;因此咱們須要稍微調整一下同步代碼來支持異常捕獲。異步
1 public class ProductApplicationService 2 { 3 public void SyncProducts() 4 { 5 Task.Factory.StartNew(SyncPrdoctsTask); 6 } 7 8 private static void SyncPrdoctsTask() 9 { 10 try 11 { 12 var productColl = DominModel.Products.GetActivateProducts(); 13 if (!productColl.Any()) return; 14 15 DominModel.Products.WriteProudcts(productColl); 16 } 17 catch (Exception exception) 18 { 19 //記錄下來... 20 } 21 } 22 }
若是你裝了像Resharp這樣的輔助插件的話會對你重構代碼頗有幫助,提取某一個方法會很方便快捷;async
上述代碼中,就在新開的線程中包含了異常捕獲的代碼;這樣就不會致使你程序拋出不少未處理異常,在重要的邏輯點可能會丟失數據。不是說全部的異常都應該由框架來處理,咱們須要本身手動的控制某個邏輯點的異常,這樣咱們能夠保證咱們本身的邏輯可以繼續運行下去。有些邏輯是不可能由於異常的出現而終止整個處理過程的。
位於SOA服務的最外層服務接口時,一般都須要包裝內部衆多服務接口來組合出外部須要的數據,此時須要查詢不少接口的數據,而後等待數據都到齊了以後再將其統一的返回給前端。因爲我有一段時間是專門給前端H5提供接口的,最讓我感觸的就是服務接口須要整合全部的數據給前端,從用戶的角度講不但願手機的界面還出現異步的現象吧,畢竟就那麼大屏幕還有白的地方。可是這個需求給咱們開發人員帶來了問題,若是用順序讀取方式將數據都組合好,那個時間是人所沒法接受的,因此咱們須要開啓並行來同時讀取多個後端服務接口的數據(前提是你這些數據沒有先後依賴關係)。
1 public static ProductCollection GetProductByIds(List<long> pIds) 2 { 3 var result = new ProductCollection(); 4 5 Parallel.ForEach(pIds, id => 6 { 7 //並行方法 8 }); 9 10 return result; 11 }
一切看起來很舒服,多個ID同一個時間被一塊兒運行,可是這裏面有個坑。
若是咱們用上述代碼開啓並行後,從GetProductByIds業務點來看一切會很順利,並且效果很明顯速度很快;可是若是當前GetProductByIds方法還在處理過程當中時你再發起另外一個服務調用時你就會發現服務器響應變慢了,由於全部的請求線程所有被佔用了,這裏Parallel並無咱們想的那麼智能,能根據狀況控制線程數;咱們須要本身控制咱們並行時的最大線程數,這樣能夠防止因爲多線程被一個業務點佔用而致使服務隊列其餘的後續請求(此時看CPU不必定很高,若是CPU太高致使不接受請求能理解,可是因爲系統設置的問題讓線程數不夠用也是有可能的)
1 public static ProductCollection GetProductByIds(List<long> pIds) 2 { 3 var result = new ProductCollection(); 4 5 Parallel.ForEach(pIds, new ParallelOptions() { MaxDegreeOfParallelism = 5 /*設置最大線程數*/}, id => 6 { 7 //並行方法 8 }); 9 10 return result; 11 }
這點上我犯了兩次錯,第一次是將前端須要的數據順序打亂了,致使數據的排名出來問題;第二次是將寫入數據庫的同步數據的時間打亂了,致使程序沒法再繼續上次的結束時間繼續同步。因此請你們必定要記住,當你使用並行時,首先問本身你當前的數據上下文邏輯在不在意先後順序關係,一旦開啓並行後全部的數據都是無須的。
如今咱們提供的服務接口多多少少會用到異步async,大概就是想讓咱們的系統可以提到點併發量,讓寶貴的請求處理線程可以及時的被系統再利用而不是在等待上浪費。
大概代碼會是這樣的,服務入口:
1 public async Task<int> OperationProduct(long ids) 2 { 3 return await DominModel.Products.OperationProduct(ids); 4 }
業務邏輯:
1 public static async Task<int> OperationProduct(long ids) 2 { 3 return await Task.Factory.StartNew<int>(() => 4 { 5 System.Threading.Thread.Sleep(5000); 6 return 100; 7 8 //其實這裏開啓的線程是請求線程池中的請求處理線程,說白了這樣並不會提升併發等於沒用。 9 }); 10 }
其實當咱們最後開啓了一個新線程時,這個新的線程和你awit的線程是同一種類型,這樣並不會提升併發反而會因爲頻繁的切換線程影響性能。要想真的讓你的async有實際意義,使用手動開啓新線程來提升併發。(前提是你瞭解了當前系統的總體CPU和線程的比例,也就是說你開啓一個兩個手動線程是不會有問題的,可是你要放在併發的入口上就請慎重考慮)
在Task中開啓手動線程有一點麻煩,看代碼:
1 public async Task<int> OperationProduct(long id) 2 { 3 var funResult = new AWaitTaskResultValues<int>(); 4 return await DominModel.Products.OperationProduct(id, funResult); 5 } 6 7 public static Task<int> OperationProduct(long id, AWaitTaskResultValues<int> result) 8 { 9 var taskMock = new Task<int>(() => { return 0; });//只是一個await模擬對象,主要是讓系統回收當前「請求處理線程」 10 11 var thread = new Thread((threadIds) => 12 { 13 Thread.Sleep(7000); 14 15 result.ResultValue = 100; 16 17 taskMock.Start();//因爲沒有任何的邏輯,因此處理會很快完成。 18 }); 19 20 thread.Start(); 21 22 return taskMock; 23 }
之因此這麼麻煩是爲了讓系統釋放await線程而不是阻塞該線程。我經過簡單的測試可使用少許的線程來處理更多的併發請求。