溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

.NET服務(wù)端開發(fā)—多線程使用小結(jié)(多線程使用常識(shí))

發(fā)布時(shí)間:2020-07-21 13:30:19 來源:網(wǎng)絡(luò) 閱讀:5243 作者:王清培 欄目:編程語言

有一段時(shí)間沒有更新博客了,最近半年都在著寫書《.NET框架設(shè)計(jì)—大型企業(yè)級(jí)框架設(shè)計(jì)藝術(shù)》,很高興這本書將于今年的10月份由圖靈出版社出版,有關(guān)本書的具體介紹等書要出版的時(shí)候我在另寫一篇文行做介紹??梢韵韧嘎兑幌拢緯遣┲鞫嗄陙韺?duì)應(yīng)用框架學(xué)習(xí)的總結(jié),里面包含了十幾個(gè)重量級(jí)框架模式,這些模式都是我們目前所經(jīng)常使用到的,對(duì)于學(xué)習(xí)框架和框架開發(fā)來說是很好的參考資料,大家敬請(qǐng)期待。

好了,進(jìn)入文章主題。

最近幾個(gè)月本人一直從事著SOA服務(wù)開發(fā)工作,簡(jiǎn)單點(diǎn)講就是提供服務(wù)接口的;從提供前端接口WEBAPI,到提供后端接口WCF\SOAFramework,期間學(xué)到了不少有關(guān)多線程使用上的經(jīng)驗(yàn),這些經(jīng)驗(yàn)有的是本人自己的錯(cuò)誤使用后的經(jīng)驗(yàn),有些是公司的前輩的指點(diǎn),總之這些東西你不遇到過你是不會(huì)意識(shí)到該如何使用的,所以本人覺得很有必要總結(jié)分享給廣大和我一樣工作在一線的博友們。

我們從服務(wù)的處理環(huán)節(jié)為順序來介紹:

1.使用入口線程來處理超長(zhǎng)時(shí)間調(diào)用:

任何服務(wù)的調(diào)用都需要首先進(jìn)到服務(wù)的入口方法中,該方法通常扮演著領(lǐng)域邏輯的門面接口(將系統(tǒng)用例進(jìn)行服務(wù)接口的劃分),通過該接口進(jìn)行用例的調(diào)用。當(dāng)我們需要處理長(zhǎng)時(shí)間過程時(shí)都會(huì)面臨著頭疼的超時(shí)異常,如果我們?cè)偃ピO(shè)計(jì)如何做超時(shí)補(bǔ)償措施就會(huì)很復(fù)雜而且是沒有必要的開銷。長(zhǎng)時(shí)處理的服務(wù)調(diào)用場(chǎng)景多半在同步數(shù)據(jù)中,通過某個(gè)JobWs(工作服務(wù))定期的來同步數(shù)據(jù)(本人就是在這個(gè)過程中學(xué)到的),當(dāng)我們無法預(yù)知我們的服務(wù)會(huì)處理多長(zhǎng)時(shí)間時(shí),基本上都會(huì)首先去設(shè)置調(diào)用端的連接超時(shí)時(shí)間(是不是都會(huì)這么想?);這很正常,很來超時(shí)時(shí)間就是用來給我們用的;但是我們忽視了我們當(dāng)前的業(yè)務(wù)場(chǎng)景了,如果你的服務(wù)不返回任何有關(guān)狀態(tài)值的話“其實(shí)應(yīng)該開啟一個(gè)獨(dú)立的線程來處理同步邏輯而讓服務(wù)的調(diào)用者盡早收到相應(yīng)”。

public class ProductApplicationService
    {
        public void SyncProducts()
        {
            Task.Factory.StartNew(() 
=>
            {
                var productColl = 
DominModel.Products.GetActivateProducts();
                if 
(!productColl.Any()) return; 
                DominModel.Products.WriteProudcts(productColl);
            });
        }
    }

這樣就可以盡早解放調(diào)用者;通過開啟一的單獨(dú)的線程來處理具體的同步邏輯。

如果你的服務(wù)需要返回某個(gè)狀態(tài)值怎么辦?其實(shí)我們可以參考”異步消息架構(gòu)模式“來將消息寫入到某個(gè)消息隊(duì)列中,然后客戶端定期來取或者推送都可以,讓當(dāng)前的這個(gè)服務(wù)方法能夠平滑的處理,至少為系統(tǒng)的整體性能瓶頸做了一份貢獻(xiàn)。

1.1異常處理:

入口位置通常都會(huì)記錄下調(diào)用的異常信息,也就是加上一個(gè)try{}catch{},用來捕獲本次調(diào)用的所有異常信息。(當(dāng)然你可能會(huì)說代碼中充斥著try{}catch{}不是很好,可以將其放到某個(gè)看不見的地方自動(dòng)處理,這有好有壞,看不見的地方我們就必然少不了配置,少不了對(duì)自定義異常類型的配置,總之事物都有兩面性。)

public class ProductApplicationService
{
    public void 
SyncProducts()
    {
        try
        {
            Task.Factory.StartNew(() =>
            {
                var 
productColl = DominModel.Products.GetActivateProducts();
                if 
(!productColl.Any()) return; 
                DominModel.Products.WriteProudcts(productColl);
            });
        }
        catch(Exception exception)
        {
            //記錄下來...
        }
    }
}

像這樣,看上去好像沒問題哦,但是我們仔細(xì)看看就會(huì)發(fā)現(xiàn),這個(gè)try{}catch{}根本捕獲不到我們?nèi)魏萎惓P畔⒌?,因?yàn)檫@個(gè)方法是在我們開啟的線程外面的,也就是說它早就結(jié)束了,開啟的線程處理?xiàng)V懈揪蜎]有任何的try{}catch{}機(jī)制代碼了;所以我們需要稍微調(diào)整一下同步代碼來支持異常捕獲。

public class ProductApplicationService
{
    public void 
SyncProducts()
    {
        Task.Factory.StartNew(SyncPrdoctsTask);
    } 
    private static void SyncPrdoctsTask()
    {
        try
        {
            var productColl = 
DominModel.Products.GetActivateProducts();
            if 
(!productColl.Any()) return; 
            DominModel.Products.WriteProudcts(productColl);
        }
        catch 
(Exception exception)
        {
            //記錄下來...
        }
    }
}

如果你裝了像Resharp這樣的輔助插件的話會(huì)對(duì)你重構(gòu)代碼很有幫助,提取某一個(gè)方法會(huì)很方便快捷;

上述代碼中,就在新開的線程中包含了異常捕獲的代碼;這樣就不會(huì)導(dǎo)致你程序拋出很多未處理異常,在重要的邏輯點(diǎn)可能會(huì)丟失數(shù)據(jù)。不是說所有的異常都應(yīng)該由框架來處理,我們需要自己手動(dòng)的控制某個(gè)邏輯點(diǎn)的異常,這樣我們可以保證我們自己的邏輯能夠繼續(xù)運(yùn)行下去。有些邏輯是不可能因?yàn)楫惓5某霈F(xiàn)而終止整個(gè)處理過程的。

2.利用并行來提高多組數(shù)據(jù)的讀取

位于SOA服務(wù)的最外層服務(wù)接口時(shí),通常都需要包裝內(nèi)部眾多服務(wù)接口來組合出外部需要的數(shù)據(jù),此時(shí)需要查詢很多接口的數(shù)據(jù),然后等待數(shù)據(jù)都到齊了之后再將其統(tǒng)一的返回給前端。由于我有一段時(shí)間是專門給前端H5提供接口的,最讓我感觸的就是服務(wù)接口需要整合所有的數(shù)據(jù)給前端,從用戶的角度講不希望手機(jī)的界面還出現(xiàn)異步的現(xiàn)象吧,畢竟就那么大屏幕還有白的地方。但是這個(gè)需求給我們開發(fā)人員帶來了問題,如果用順序讀取方式將數(shù)據(jù)都組合好,那個(gè)時(shí)間是人所無法接受的,所以我們需要開啟并行來同時(shí)讀取多個(gè)后端服務(wù)接口的數(shù)據(jù)(前提是你這些數(shù)據(jù)沒有前后依賴關(guān)系)。

public static ProductCollection GetProductByIds(List<long> 
pIds)
{
    var result = new ProductCollection(); 
    Parallel.ForEach(pIds, id =>
    {
        //并行方法
    }); 
    return result;
}

一切看起來很舒服,多個(gè)ID同一個(gè)時(shí)間被一起運(yùn)行,但是這里面有個(gè)坑。

2.1控制并行線程數(shù):

如果我們用上述代碼開啟并行后,從GetProductByIds業(yè)務(wù)點(diǎn)來看一切會(huì)很順利,而且效果很明顯速度很快;但是如果當(dāng)前GetProductByIds方法還在處理過程中時(shí)你再發(fā)起另一個(gè)服務(wù)調(diào)用時(shí)你就會(huì)發(fā)現(xiàn)服務(wù)器響應(yīng)變慢了,因?yàn)樗械恼?qǐng)求線程全部被占用了,這里Parallel并沒有我們想的那么智能,能根據(jù)情況控制線程數(shù);我們需要自己控制我們并行時(shí)的最大線程數(shù),這樣可以防止由于多線程被一個(gè)業(yè)務(wù)點(diǎn)占用而導(dǎo)致服務(wù)隊(duì)列其他的后續(xù)請(qǐng)求(此時(shí)看CPU不一定很高,如果CPU過高導(dǎo)致不接受請(qǐng)求能理解,但是由于系統(tǒng)設(shè)置的問題讓線程數(shù)不夠用也是有可能的)

public static ProductCollection GetProductByIds(List<long> 
pIds)
{
    var result = new ProductCollection(); 
    Parallel.ForEach(pIds, new ParallelOptions() { 
MaxDegreeOfParallelism = 5 /*設(shè)置最大線程數(shù)*/}, id =>
    {
        //并行方法
    }); 
    return result;
}

2.2使用并行處理時(shí)數(shù)據(jù)的前后順序是第一原則

這點(diǎn)上我犯了兩次錯(cuò),第一次是將前端需要的數(shù)據(jù)順序打亂了,導(dǎo)致數(shù)據(jù)的排名出來問題;第二次是將寫入數(shù)據(jù)庫(kù)的同步數(shù)據(jù)的時(shí)間打亂了,導(dǎo)致程序無法再繼續(xù)上次的結(jié)束時(shí)間繼續(xù)同步。所以請(qǐng)大家一定要記住,當(dāng)你使用并行時(shí),首先問自己你當(dāng)前的數(shù)據(jù)上下文邏輯在不在乎前后順序關(guān)系,一旦開啟并行后所有的數(shù)據(jù)都是無須的。

3.手動(dòng)開啟一個(gè)線程來代替并行庫(kù)啟動(dòng)的線程

現(xiàn)在我們提供的服務(wù)接口多多少少會(huì)用到異步async,大概就是想讓我們的系統(tǒng)能夠提到點(diǎn)并發(fā)量,讓寶貴的請(qǐng)求處理線程能夠及時(shí)的被系統(tǒng)再利用而不是在等待上浪費(fèi)。

大概代碼會(huì)是這樣的,服務(wù)入口:

public async Task<int> OperationProduct(long ids)
{
    return await DominModel.Products.OperationProduct(ids);
}

業(yè)務(wù)邏輯:

public static async Task<int> OperationProduct(long 
ids)
{
    return await Task.Factory.StartNew<int>(() =>
      {
          System.Threading.Thread.Sleep(5000);
          return 100; 
          //其實(shí)這里開啟的線程是請(qǐng)求線程池中的請(qǐng)求處理線程,說白了這樣并不會(huì)提高并發(fā)等于沒用。
      });
}

其實(shí)當(dāng)我們最后開啟了一個(gè)新線程時(shí),這個(gè)新的線程和你awit的線程是同一種類型,這樣并不會(huì)提高并發(fā)反而會(huì)由于頻繁的切換線程影響性能。要想真的讓你的async有實(shí)際意義,使用手動(dòng)開啟新線程來提高并發(fā)。(前提是你了解了當(dāng)前系統(tǒng)的整體CPU和線程的比例,也就是說你開啟一個(gè)兩個(gè)手動(dòng)線程是不會(huì)有問題的,但是你要放在并發(fā)的入口上就請(qǐng)慎重考慮)

在Task中開啟手動(dòng)線程有一點(diǎn)麻煩,看代碼:

public async Task<int> OperationProduct(long 
id)
{
    var funResult = new AWaitTaskResultValues<int>();
    return await DominModel.Products.OperationProduct(id, funResult);
} 
public static Task<int> OperationProduct(long id, 
AWaitTaskResultValues<int> result)
        {
            var 
taskMock = new Task<int>(() => { return 0; 
});//只是一個(gè)await模擬對(duì)象,主要是讓系統(tǒng)回收當(dāng)前“請(qǐng)求處理線程” 
            var thread = new Thread((threadIds) 
=>
            {
                Thread.Sleep(7000); 
                result.ResultValue = 100; 
                taskMock.Start();//由于沒有任何的邏輯,所以處理會(huì)很快完成。
            }); 
            thread.Start(); 
            return taskMock;
        }

之所以這么麻煩是為了讓系統(tǒng)釋放await線程而不是阻塞該線程。我通過簡(jiǎn)單的測(cè)試可以使用少量的線程來處理更多的并發(fā)請(qǐng)求。


作者:王清培

出處:http://wangqingpei557.blog.51cto.com/

本文版權(quán)歸作者和51CTO共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁(yè)面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI