最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

第 61 講:多線程(三):線程池

2021-10-10 10:38 作者:SunnieShine  | 我要投稿

前面兩節(jié)我們使用了理論給大家介紹了一下多線程的基本概念,以及多線程的不安全性,以及使用 lock 簡(jiǎn)單告訴大家如何讓非原子操作改成原子操作。今天我們來(lái)說一下線程池的基本使用方式。

Part 1 為什么要有線程池的概念

線程是一個(gè)好東西,它確實(shí)可以幫助我們開設(shè)額外的運(yùn)行過程,讓每一個(gè)線程都能夠并行執(zhí)行程序(宏觀來(lái)說),來(lái)提高程序執(zhí)行效率。不過問題在于,我們開設(shè)的線程都使用的是 Thread 的實(shí)例,并使用委托類型對(duì)象來(lái)完成對(duì)指定方法的執(zhí)行。

問題就在這里。線程開設(shè)需要使用 new Thread 的代碼來(lái)完成,但這樣無(wú)疑會(huì)在底層開辟內(nèi)存空間,導(dǎo)致程序耗費(fèi)內(nèi)存,影響空間消耗;另外,開辟內(nèi)存空間等行為也都會(huì)耗費(fèi)時(shí)間,不是說光 new Thread 看起來(lái)這么簡(jiǎn)單。

實(shí)際上,開辟一個(gè)線程所需要的內(nèi)存空間,然后再來(lái)執(zhí)行的話,有些時(shí)候甚至是可以感受到卡頓的;也就是說,這種過程是有可見的副作用的。因此,線程池的概念就誕生了。

所謂的線程池(Thread Pool),這個(gè)術(shù)語(yǔ)詞看起來(lái)有點(diǎn)奇特,但也很形象。線程池就是一個(gè)邏輯上的池子,它里面預(yù)先就放好了一個(gè)已經(jīng)給你分配好了的后臺(tái)線程序列。這些后臺(tái)線程序列其實(shí)是一個(gè)一個(gè)的模板一樣的存在,它們自身不是拿來(lái)直接運(yùn)行的,而是你在調(diào)出其中的一個(gè)線程后,然后給予它的執(zhí)行行為,就可以直接執(zhí)行了,是這么一個(gè)意思。

線程池的好處就是在于如下兩點(diǎn):

  1. 它是后臺(tái)線程的序列構(gòu)成的,這意味著我們創(chuàng)建的這些線程整體會(huì)受到程序本身關(guān)閉而自動(dòng)終結(jié),無(wú)需考慮出現(xiàn)異常情況的時(shí)候無(wú)法終結(jié)的問題;

  2. 它是預(yù)先分配好內(nèi)存空間的線程序列,因此我們無(wú)需初始化即可調(diào)用,這樣省去了內(nèi)存分配需要用的時(shí)間。

Part 2 線程池的使用

線程池的使用相當(dāng)簡(jiǎn)單,甚至簡(jiǎn)單到有手就行。之前我們用的是 new Thread,然后在構(gòu)造器里面?zhèn)魅胍粋€(gè) ThreadStart 這個(gè)委托類型的實(shí)例,并帶上我們需要的方法即可?,F(xiàn)在我們有了線程池后,我們需要用一個(gè)叫做 ThreadPool.QueueUserWorkItem 方法來(lái)完成這個(gè)任務(wù)。

假設(shè)我們有這樣一個(gè)執(zhí)行起來(lái)很慢的操作:

我們要讓這個(gè) DoWork 方法以線程形式執(zhí)行的話,我們這么調(diào)用:

稍微更長(zhǎng)了一點(diǎn),但只是代碼更長(zhǎng)了一點(diǎn),但效果是一樣的,而且提升了性能。不過……直接這么寫,我們發(fā)現(xiàn)編譯器會(huì)給出錯(cuò)誤:

可以發(fā)現(xiàn),此時(shí)編譯器告訴我們,DoWork 方法無(wú)法匹配這個(gè)叫做 WaitCallBack 的委托類型對(duì)象。我們點(diǎn)開 WaitCallBack 委托類型,可以發(fā)現(xiàn)它必須傳入一個(gè) object 類型的參數(shù)進(jìn)去。

稍微說一下,這個(gè) object? 的問號(hào)表示這個(gè)傳入的 object 類型的參數(shù)可以為 null 值。這個(gè)語(yǔ)法不是 C# 原本就有的語(yǔ)法,包括上面的 namespace System.Threading; 也不是 C# 的基本語(yǔ)法。它們分別在 C# 8 和 C# 10 里才可以使用,但這些語(yǔ)法我們以后再來(lái)說,這里你稍微了解一下就行了,這樣也避免了發(fā)現(xiàn)這樣不懂的語(yǔ)法然后跑來(lái)問我。

你放心,這些語(yǔ)法我們以后會(huì)慢慢說到的,所以不要擔(dān)心。

這里我們注意到,它有一個(gè) object 類型的參數(shù),這個(gè)參數(shù)名字叫 state。在文本注釋上對(duì)這個(gè)參數(shù)寫的解釋是“state 是一個(gè)對(duì)象,它包含一些基本信息,可以在這個(gè)回調(diào)函數(shù)里使用”??伞睦飦?lái)的回調(diào)函數(shù)?這個(gè) WaitCallBack 的委托類型里通過 += 添加進(jìn)去的方法序列,都是我們的回調(diào)函數(shù),這個(gè)在委托里講過了,你沒忘吧?可……這也沒用 += 符號(hào)啊……emmm,你是不是忘了一點(diǎn)。初始化委托對(duì)象是用的 new 來(lái)完成的;而這個(gè) new 里傳入了一個(gè)初始參數(shù)的;而這個(gè)參數(shù)是不是就是一個(gè)方法名?這個(gè)方法名就是初始值,它已經(jīng)在初始情況下就加入到了委托的回調(diào)函數(shù)列表里了。它也等價(jià)于你這么寫:

只是,這么寫要復(fù)雜一點(diǎn)。所以我們直接把 DoWork 給傳入到參數(shù)里了。這些都是委托的基本語(yǔ)法,沒有忘吧?而這里的 state 就是恰好用于這個(gè) DoWork 方法的;當(dāng)然,你也可以不用它,也可以用它,這個(gè)隨便你;只不過……這個(gè)參數(shù)必須傳入,因?yàn)槲蓄愋鸵笪覀儌魅胍粋€(gè) object 類型的參數(shù)進(jìn)去。

隨后,我們加入?yún)?shù) object state 進(jìn)去。

這樣就可以了??傊@樣我們就模擬好了一個(gè)完整的使用線程池調(diào)用線程的方法了。這個(gè) state 我們沒有用到,因?yàn)槟壳皝?lái)說它是無(wú)用的。

然后,我們?cè)囍?Main 方法里加上一些別的代碼,模擬一下讓代碼卡起來(lái)才使用多線程技術(shù)的過程:

我們來(lái)看一下,這個(gè)代碼到底是什么意思。

首先,我們?cè)诘?3 行使用 ThreadPool.QueueUserWorkItem 靜態(tài)方法來(lái)調(diào)取一個(gè)線程池里的后臺(tái)線程,并讓其執(zhí)行 DoWork 方法。接著,我們?cè)谥骶€程里寫上了 for 循環(huán),等同的代碼,不過這次在主線程里輸出的是減號(hào),在線程池線程里輸出的是加號(hào)。

接著,在第 15 行代碼里我們使用了一個(gè)全新的方法:Thread.Sleep 靜態(tài)方法。這個(gè)方法用在指定的線程的代碼上,用于讓這個(gè)指定的線程卡頓,停止執(zhí)行指定的時(shí)長(zhǎng)。單位是毫秒(1000 毫秒等于 1 秒,所以示例程序里是讓程序等待 1 秒的意思)。因?yàn)槌绦蜉敵?1000 個(gè)加號(hào)和 1000 個(gè)減號(hào)的總時(shí)長(zhǎng)也用不到 1 秒,所以為了讓大家能夠看到兩個(gè)線程均完成執(zhí)行,我們使用了這個(gè)方法卡住主線程。如果主線程已經(jīng)輸出完成 1000 個(gè)減號(hào)而線程池線程還沒有完成 1000 個(gè)加號(hào)的輸出的話,這行代碼會(huì)卡住主線程,讓主線程暫時(shí)不會(huì)終止掉。按照正常的邏輯來(lái)說,由于線程池的線程都是后臺(tái)線程,所以一旦主線程終止運(yùn)行了(不論是異常終止的還是正確執(zhí)行完成而終止的),這些后臺(tái)線程全部都會(huì)停止執(zhí)行。而此時(shí),因?yàn)楹笈_(tái)線程可能還沒有完成對(duì) 1000 個(gè)加號(hào)的輸出就停止了,就達(dá)不到我們這里的顯示要求,因此我們故意卡住主線程,讓主線程必須等待 1 秒鐘后才能讓程序繼續(xù)往下執(zhí)行;而下面沒有別的代碼了,所以寫在最后的這個(gè) Thread.Sleep 就表示主線程在 1 秒后才會(huì)退出。

為什么是 1 秒呢?因?yàn)?1 秒比較合適,不算極限時(shí)長(zhǎng),也不用卡住主線程過長(zhǎng)時(shí)間。輸出 2000 個(gè)符號(hào)時(shí)長(zhǎng)壓根到不了一秒鐘,但如果你寫兩秒的話,可以發(fā)現(xiàn)程序卡住,兩秒后才會(huì)恢復(fù)繼續(xù)執(zhí)行,所以完全沒有必要多卡住一秒鐘的時(shí)間,這多出來(lái)的時(shí)間就浪費(fèi)了。所以選擇 1 秒比較合適;相反,如果你寫的時(shí)長(zhǎng)比 1 秒要少的話,可能程序還沒有真正完成對(duì) 2000 個(gè)符號(hào)的輸出,等待時(shí)長(zhǎng)就結(jié)束了,然后主線程就停止執(zhí)行了,而此時(shí)符號(hào)還沒有完成輸出,這就沒有達(dá)到我們的目的。

因?yàn)槭嵌嗑€程完成的操作,所以輸出結(jié)果是這樣的:

當(dāng)然,這是其中一種情況而已。因?yàn)槎嗑€程具有不可再現(xiàn)性,所以無(wú)法決定輸出的加號(hào)和減號(hào)的先后順序和相對(duì)輸出個(gè)數(shù)的情況。

從這個(gè)圖里我們還可以看出,線程池的線程是自動(dòng)啟動(dòng)的,一旦調(diào)用 QueueUserWorkItem 方法后,線程就會(huì)自動(dòng)開始執(zhí)行

Part 3 不要隨便把 Thread.Sleep 用于主線程

主線程是一個(gè)神奇的東西??赡苣阍诳刂婆_(tái)程序里看不出來(lái)它有什么效果,但是在 UI(用戶接口,說白了就是界面程序)上就不一樣了。在微軟的資料里,比如 Windows Form 這樣的帶界面的程序框架下,繪制和書寫代碼得到的窗體都是單線程的;這意味著你一旦使用 Thread.Sleep 方法在主線程上,就必然會(huì)卡死主線程,因?yàn)閱尉€程的程序,UI 只會(huì)存在于主線程上;而你一旦在主線程上隨便哪個(gè)代碼的位置上加一句 Thread.Sleep 方法的調(diào)用,那么 UI 就百分之百會(huì)被卡住。所以,Thread.Sleep 一般不要隨便用??刂婆_(tái)的程序不會(huì)卡住,是因?yàn)樗且粋€(gè)底層比較復(fù)雜的存在,它避免了 UI 這類情況,因此我們剛才即使用了 Thread.Sleep 也可以發(fā)現(xiàn),控制臺(tái)程序的光標(biāo)仍然在閃爍,這就說明程序沒有卡住。

所以,不要以為控制臺(tái)程序沒卡住,UI 程序就不會(huì)卡住。所以不要隨便用。

Part 4 state 參數(shù)的用途

剛才我們沒有說明 state 參數(shù)的具體用法,下面我們來(lái)說一下線程池的該參數(shù)的用法。其實(shí)這個(gè)參數(shù)是為了和外部交互而產(chǎn)生的一個(gè)額外的“附加信息”。如果你想要讓這個(gè)程序靈活起來(lái),可能就需要從外部傳入該參數(shù)。

用法是這樣的。假如我們變更一下剛才的邏輯,在 DoWork 里本來(lái)應(yīng)該固定輸出加號(hào),現(xiàn)在我們想要自定義輸出的符號(hào),那么我們需要借助 state 參數(shù)來(lái)完成。我們改寫 DoWork 方法:

我們使用參數(shù),在最開頭判斷一下,是否 statechar 類型的參數(shù)。如果不是的話,DoWork 方法直接自動(dòng)終止;如果是一個(gè)字符的話,那么我們就使用強(qiáng)制轉(zhuǎn)換來(lái)得到 element 變量,然后在循環(huán)里使用它。

接著,改變 DoWork 的參數(shù)后,我們也得增加參數(shù)吧。于是 QueueUserWorkItem 里也一樣,需要追加一個(gè)參數(shù)。

我們直接在 QueueUserWorkItem 方法參數(shù)表列上增加一個(gè)參數(shù)。注意,這個(gè)參數(shù)不是寫在 new WaitCallBack 的小括號(hào)里,而是寫在 QueueUserWorkItem 的小括號(hào)里,因?yàn)槲覀冞@個(gè) '*' 的字符參數(shù)不是 WaitCallBack 委托類型本身的合法的參數(shù),所以 QueueUserWorkItem 方法提供了一個(gè)重載,帶上這個(gè)參數(shù)到第二個(gè)參數(shù)位置上即可。

此時(shí),我們?cè)賮?lái)看運(yùn)行程序,別的地方都不改,就變動(dòng)這樣一點(diǎn)內(nèi)容,看看效果:

很好,效果達(dá)到了,這就是我們使用 state 參數(shù)的情況。

Part 5 順帶說一下 new Thread 也傳入額外參數(shù)的情況

既然我們說到了 state 參數(shù)用來(lái)交互傳參的情況,那么我們就應(yīng)該說一下 new Thread 這個(gè)情況下的外部傳參的情況。雖然這個(gè)點(diǎn)在之前就應(yīng)該說了,但因?yàn)榍懊嫖覀冇貌簧希跃蜎]提到。

之前我們用的是 ThreadStart 委托類型對(duì)象作為參數(shù)的情況。因?yàn)榇藭r(shí)我們模擬的情況也需要從外部傳入?yún)?shù),因此這個(gè)委托類型就不夠用了。這里我們用的是一個(gè)叫 ParameterizedThreadStart 的委托類型對(duì)象。這個(gè)委托類型的簽名和前面的 WaitCallBack 的委托的簽名是完全一樣的,它們沒有區(qū)別,所以你甚至連方法的簽名都不用改,只需要用 new Thread 來(lái)改一下這個(gè)就可以了。

注意代碼的第 3 行和第 4 行。第 3 行我們實(shí)例化一個(gè) Thread 類型的對(duì)象,但此時(shí)參數(shù)改為了 ParameterizedThreadStart 的委托類型的實(shí)例。接著,我們執(zhí)行使用 .Start 的時(shí)候,傳入這個(gè)我們從外部交互進(jìn)去的參數(shù)。

因?yàn)檫@個(gè)線程創(chuàng)建方式默認(rèn)創(chuàng)建的是前臺(tái)線程,所以這里的 Thread.Sleep 是可以不寫的,因?yàn)槌绦蚪K止需要等待所有線程全部完成執(zhí)行才會(huì)終止,因此不論這個(gè)創(chuàng)建出來(lái)的前臺(tái)線程先完成還是主線程先完成,所有線程都得全部完成執(zhí)行后,程序才會(huì)終止,因此無(wú)需擔(dān)心先后順序。但需要注意的是,這個(gè)不是線程池,因此要手動(dòng)調(diào)用 Start 方法來(lái)開始啟動(dòng)線程執(zhí)行。

最后我們還是來(lái)看一下結(jié)果:

這個(gè) ParameterizedThreadStart 委托類型里的 parameterized 單詞,是“參數(shù)化”的意思。從術(shù)語(yǔ)詞上可能看不懂這個(gè)詞語(yǔ)為啥用在這里,但實(shí)際上參數(shù)化的實(shí)際含義是讓一個(gè)東西能夠以參數(shù)的行為和形式傳入的這么一種情況。顯然這里是滿足這個(gè)說法的要求的:我們需要一個(gè)參數(shù)來(lái)交互從外部傳入的內(nèi)容。


第 61 講:多線程(三):線程池的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
大邑县| 利辛县| 贡山| 湛江市| 安陆市| 清水河县| 福建省| 彰武县| 三穗县| 贞丰县| 揭东县| 烟台市| 乳源| 聂拉木县| 永仁县| 拉萨市| 翁牛特旗| 临桂县| 丁青县| 榆中县| 新疆| 漳浦县| 旺苍县| 东阿县| 祁连县| 博爱县| 公主岭市| 南溪县| 武义县| 黔南| 临桂县| 西峡县| 武乡县| 黔西县| 杨浦区| 崇州市| 佳木斯市| 文登市| 普兰县| 阿勒泰市| 图们市|