有一段時間沒有更新博客了,最近半年都在着寫書《.NET框架設計—大型企業級框架設計藝術》,很高興這本書將於今年的10月份由圖靈出版社出版,有關本書的具體介紹等書要出版的時候我在另寫一篇文行做介紹。可以先透露一下,本書是博主多年來對應用框架學習的總結,里面包含了十幾個重量級框架模式,這些模式都是我們目前所經常使用到的,對於學習框架和框架開發來說是很好的參考資料,大家敬請期待。
好了,進入文章主題。
最近幾個月本人一直從事着SOA服務開發工作,簡單點講就是提供服務接口的;從提供前端接口WEBAPI,到提供后端接口WCF\SOAFramework,期間學到了不少有關多線程使用上的經驗,這些經驗有的是本人自己的錯誤使用后的經驗,有些是公司的前輩的指點,總之這些東西你不遇到過你是不會意識到該如何使用的,所以本人覺得很有必要總結分享給廣大和我一樣工作在一線的博友們。
我們從服務的處理環節為順序來介紹:
1.使用入口線程來處理超長時間調用:
任何服務的調用都需要首先進到服務的入口方法中,該方法通常扮演着領域邏輯的門面接口(將系統用例進行服務接口的划分),通過該接口進行用例的調用。當我們需要處理長時間過程時都會面臨着頭疼的超時異常,如果我們再去設計如何做超時補償措施就會很復雜而且是沒有必要的開銷。長時處理的服務調用場景多半在同步數據中,通過某個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 }
這樣就可以盡早解放調用者;通過開啟一的單獨的線程來處理具體的同步邏輯。
如果你的服務需要返回某個狀態值怎么辦?其實我們可以參考”異步消息架構模式“來將消息寫入到某個消息隊列中,然后客戶端定期來取或者推送都可以,讓當前的這個服務方法能夠平滑的處理,至少為系統的整體性能瓶頸做了一份貢獻。
1.1異常處理:
入口位置通常都會記錄下調用的異常信息,也就是加上一個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這樣的輔助插件的話會對你重構代碼很有幫助,提取某一個方法會很方便快捷;
上述代碼中,就在新開的線程中包含了異常捕獲的代碼;這樣就不會導致你程序拋出很多未處理異常,在重要的邏輯點可能會丟失數據。不是說所有的異常都應該由框架來處理,我們需要自己手動的控制某個邏輯點的異常,這樣我們可以保證我們自己的邏輯能夠繼續運行下去。有些邏輯是不可能因為異常的出現而終止整個處理過程的。
2.利用並行來提高多組數據的讀取
位於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同一個時間被一起運行,但是這里面有個坑。
2.1控制並行線程數:
如果我們用上述代碼開啟並行后,從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 }
2.2使用並行處理時數據的前后順序是第一原則
這點上我犯了兩次錯,第一次是將前端需要的數據順序打亂了,導致數據的排名出來問題;第二次是將寫入數據庫的同步數據的時間打亂了,導致程序無法再繼續上次的結束時間繼續同步。所以請大家一定要記住,當你使用並行時,首先問自己你當前的數據上下文邏輯在不在乎前后順序關系,一旦開啟並行后所有的數據都是無須的。
3.手動開啟一個線程來代替並行庫啟動的線程
現在我們提供的服務接口多多少少會用到異步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線程而不是阻塞該線程。我通過簡單的測試可以使用少量的線程來處理更多的並發請求。