C#中的線程<一>

平時我發現周圍有些人談及到C#中的線程,說這些線程不可控,或則很難控制,問其爲什麼?曰:因爲是託管線程。扯淡!!!

因此不得不讓我狠下心做一個C#線程相關的連載。

小生愚鈍,連載中的內存有些是讀書筆記有些是自己總結。如果哪裏讓你疑惑,可以留言我們討論,技術貴在分享。希望看了這個系列的朋友不要再說線程不可控制,就算我沒白折騰。

開始吧….

 

--------------------------------------------------------------------------------------------------------------------------------------------

這一節我們需要了解線程的基礎部分,便於以後更深入的討論。

 

1:在早期的計算機時期,操作系統沒有提供線程的概念。事實上在16 位 windows ,整個系統只運行着一個執行線程,很容易阻止其他任務執行,如果有程序含有bug,造成無線循環,這會造成整個機器停止工作。

2:解決這個問題:他們解決在一個進程中運行應用程序的每個實例。進程不過是應用程序的一個實例要使用的資源的一個集合。每個進程都被賦予了一個虛擬地址空間,確保一個進程使用的代碼和數據無法由另一個進程訪問。,這就確保了應用程序的健壯性。但是CPU只有一個,如果一個程序進入死循環,其他任務就不能被執行,所以系統仍然可能停止響應。

爲了解決這個問題,他們用線程解決了這個問題。作爲一個windows概念(這裏強調Windows概念的原因是這個討論的線程概念是Windows特有得,linux的線程概念和windows就有很大的不同),線程的職責是對CPU進行虛擬化。Windows爲每個進程都提供了該進程專用的線程(功能相當於一個CPU,可將線程理解成一個邏輯cpu).如果應用程序的代碼進入無限循環,只會凍結與那個代碼關聯的進程,但其他進程(因爲他們有自己的線程,也就是有自己的邏輯cpu)不會被凍結:他們會繼續執行!

線程會產生空間(內存耗用)和時間(運行時的執行性能)上的開銷:

1:在每個線程中,都有以下的要素:

a:線程內核對象:【包含對線程進行描述的屬性,以及一個線程上下文--一個內存塊】,x86CPU的計算機上運行時,線程上下文使用約700字節。對於x64和IA64CPU,上下文分別使用大約1240字節和2500字節的內存。---------存在的意義:因爲在單個CPU計算機中,一次只能做一件事情,所以Windows中的所有線程必須共享物理CPU,Windows只把一個線程分配給CPU,運行一個“時間片”,一旦時間片到期,Windows就上下文切換到另一個線程,在切換過程中Windows必須將CPU寄存器中的值保留到當前正在運行的線程的內核對象內部的一個上下文結構中,還得把下一個選中的線程的上下文結構中的值加載到CPU的寄存器中,Windows大概30毫秒執行一次切換,上下文切換爲淨開銷,也就是說,上下文切換不會換來任何內存和性能上的收益,但是通過切換實現了多線程任務並行執行和好的用戶體驗-------要構建高性能的應用程序和組建,就應該儘可能地避免上下文切換,這也是多線程程序編碼設計的一個重要因素

b:線程環境塊:【線程異常處理節點,線程本地儲存數據,和一些圖形使用數據】x86和x64cpu中是4kb,IA64CPU中是8kb

c:用戶模式棧:【儲存傳給方法的局部變量和實參,以及指出線程在方法返回的時候該從什麼地方開始執行的連接】Windows 爲每個線程分配1MB內存---這1MB內存在本地應用程序是虛擬地址控件,並沒有物理儲存,但是託管代碼會強制會強制讓Windows立即劃分出棧的物理儲存,CLR團隊這樣設計是爲了保證當系統中的可用很少時候,託管代碼具有可靠性

d:內核模式棧:【出於安全考慮,應用程序不能訪問內核模式棧,但是應用程序訪問內核模式的函數傳遞參數的時候會使用內核模式棧,此時windows會從用戶模式棧複製到內核模式棧,】32位大小爲12KB,64位大小爲:24KB

e:DLL線程連接和線程分離通知:

線程創建和線程終止都會調用那個進程中加載的所有DLL的DLLMain方法,並向該方法傳遞一個標誌,爲進程中創建和銷燬的每個線程執行一些特殊的初始化或清理操作

以上就是創建線程,讓它進駐系統以及最後銷燬它所需要全部空間和時間開銷

*C#和其他大多數託管編程語言生成的DLL沒有DLLMain函數,所以託管代碼DLL不會收到DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知,這提升了性能。除此之外,非託管DLL可調用Win32 DisableThreadLibrearyCalls函數來決定不理會這些通知,遺憾的是許多非託管開發人員不知道這個函數,所以沒有調用它

如果線程在時間片規定時間內完成了任務,它可以自主終止其時間片

在進行垃圾回收的時候,CLR必須掛起(暫停)所有線程,遍歷它們棧來查找根以便對堆中的對象進行標記,再次遍歷它們的棧,再恢復所有線程。所以減少線程數量對於垃圾會回收性能有幫助,每次使用一個調試器並遇到一個斷點,Windows都會掛起正在調試的應用程序中的所有線程,並在單步執行或者運行應用程序時恢復所有線程。因此,你使用的線程越多,調試體驗也就越差

在安裝了多個CPU或者多核CPU計算機上,Windows確保單個線程不會同時在多個內核上調度,那樣會引起巨大混亂

雖然線程會消耗大量內存和時間,性能上犧牲了不少,但是至今用線程來增強應用程序的可伸縮性,並且在現在多CPU或多核計算機的普及和硬件的大力發展,多線程的路會越走越遠

如果只關心原始性能,那麼任何計算機最優的線程數量就是那臺機器的CPU數量。超過CPU了那就會發生上下文切換,有性能損失,而Windwos設計時,決定側重可靠性和響應能力,非原始性能和速度,這樣就不會造成OS“凍死”  要不沒人會用Windows   我也不會  呵呵

在Windows中線程比進程廉價得多,所以開發人員不怎麼創建進程,而大量創建線程,打開你的任務管理器,看看佔着內存但並沒讓CPU幹活的線程一大堆,線程雖然沒進程昂貴,但是相對其他系統資源還是昂貴得多,所以能省則省了

Jeffrey  Richter 在他的《CLR via C#》一書中,用很嚴肅的語言批評了,以爲線程廉價 而胡亂大量使用的行爲...並多出用了感嘆號。作爲一個開發人員對線程的理性應用是對程序和用戶負責

多核CPU看起十分強大, 但是也帶了新的問題,比如多個內核需要併發訪問其他系統資源,這些資源就成了系統總體性能的瓶頸,比如兩個或多個內核要同時訪問RAM,這個時候內存帶寬就限制了總體性能。

爲了解決了這個問題,所以現在計算機都採用了所謂的MUMA架構如圖:

PS:如果代碼活圖片不能看到請移步:http://www.cnblogs.com/Wonder1989/archive/2013/04/02/2996515.html

32位Windows 支持安裝32個CPU,64位支持安裝64個CPU    從Windows Server R2 開始,Windows開始支持在一臺機器中使用256個邏輯處理器。

如圖:

PS:如果代碼活圖片不能看到請移步:http://www.cnblogs.com/Wonder1989/archive/2013/04/02/2996515.html

今天CLR還不能利用處理器組,所以它創建額所有線程都在處理器組0(默認組)中運行。所以目前情況下託管程序在64位Windows上只能使用64核,在32位系統上只能使用32核

雖然今天的一個CLR線程是直接對應一個Windows線程,但是CLR團隊保留了將來把它從Windows線程分離的權利,使用一個CLR邏輯線程並非一定要映射到一個物理Windows線程,據說,邏輯線程將使用比物理線程少得多的資源,所以可以在很少的物理線程上運行大量的邏輯線程,比如,CLR可以判斷你的一個線程處於等待狀態,重新分配那個線程去做一個不同的任務。遺憾的是要想實現這個方案,CLR團隊還有很多大量工作要做,所以近期不太可能看到CLR推出這個功能

針對以上所說我們應該儘量避免P/Invoke本地Windows函數,因爲這些函數對CLR線程一無所知,因爲分離了,堅持使用FCL(Framework類庫)中的類型,將來在性能提升後,你的代碼馬上就能享受這種提升

PS:如果想P/Invoke本地代碼,而且代碼必須使用當前物理操作系統的線程來執行,那麼應該調用System.Threading.Thread的靜態BeginThreadAffinity方法。線程不在需要使用物理操作系統線程運行時,可調用Thread的EndThreadAffinity方法通知CLR

一般強烈建議使用CLR線程池來調度線程,但是我們有時候必須要用到專用線程來實現我的操作,比如:

a:線程需要以非普通線程優先級運行。所有線程池線程都以普通優先級運行。雖然可以更改這個優先級,但是不建議那樣做,而且在不同線程池操作之間,對優先級的更改是無法持續的。

b:需要線程表現爲一個前臺線程,防止應用程序在線程結束它任務之前被終止。線程池的線程始終是後臺線程,如果CLR想終止進程,他們就可能被迫無法完成任務。

c:一個計算限制的任務需要長時間運行,線程池爲了判斷是否需要創建一個額外的線程,所採用的邏輯是比較複雜的,直接爲長時間運行的任務創建一個專用線程,就可以避免這個問題

d:要啓動一個線程,並可能調用Thread的Abort方法來提前終止它。

以下是Thread 的構造器原型:

PS:如果代碼不能看到請移步:http://www.cnblogs.com/Wonder1989/archive/2013/04/02/2996515.html

View Code

Start參數表示專用線程要執行的方法,這個方法必須和ParameterizedThreadStart委託的簽名匹配:

delegate void ParameterizedThreadStart(Object obj);

創建專用線程如下:

View Code

PS:Thread 還提供了一個獲取ThreadStart委託構造器,但是氣不接受任務參數,建議不是用這個構造器和委託,因爲功能十分有限,不接受參數處理

是用線程的理由:

a:可以使用線程將代碼同其他代碼隔離

b:可以使用線程來簡化編碼

c:可以使用線程來實現併發執行

Date:2012.4.9

1:使用microsoft spy++ 可以查看Windows實際記錄了每個線程被上下文切換到的次數【右鍵屬性即可看到】

2:Windows 是一種搶佔式多線程操作系統,不是實時操作系統,所以不能保證線程在發生某個事件後的一段時間裏開始運行

3:每個線程都分配了從0~31的一個優先級。

4:飢餓:只要存在可以調度的優先級31的線程,系統就永遠不會將優先級0~30的任何線程分配給cpu。。權限低級的得不到執行,就算低級線程正在執行時間片沒有結束的情況系統會立即將其掛起讓給高級線程

5:系統在啓動時,會創建一個名爲零頁線程的特殊線程。這個線程的優先級定位0,是在所有線程沒有執行的時候,將系統RAM所有的空閒頁清零

6:下圖總結了進程的優先級類和線程的相對優先級與優先級(0~31)的映射關係:

PS:如果代碼活圖片不能看到請移步:http://www.cnblogs.com/Wonder1989/archive/2013/04/02/2996515.html

因爲normal 是最常用的 所以系統中大多數線程優先級都是8

7:當一個線程需要長時間執行計算限制任務,一般應該降低改線程的優先級。相反如果運行非常短暫時間,再恢復爲等待狀態,則應該提高該線程優先級。比如windows微標鍵就是一個優先級很高的線程

8:線程池和本地現成創建默認爲後臺線程,但是顯示調用Thread創建後默認爲前臺

9:要儘量避免使用前臺線程,不然會發生進程【應用程序】始終終止不了的情況,前臺線程應該用於執行確實想完成的任務

如下提現了前臺線程和後臺線程的區別:

View Code

今天就到這吧。下一節我們繼續線程池的講解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章