《ASP.NET本質論》 線程基礎

       線程基礎

        在單CPU的情況下,顯然計算機同時只能執行一個程序,早期的DOS操作系統就是單任務操作系統,在那個年代,我們在運行一個程序的時候,就不能再運行第二個程序,必須退出當前的程序之後,才能運行另外的程序。到了Windows 3.1的時代,開始採用稱爲協同多任務的機制,實際上,Windows運行的多個程序並沒有真正的同時運行,每個程序都要在適當的時候釋放CPU的控制權,以便其他的程序得到執行的機會,這種機制稱爲協同。此時,只要一個程序死掉,那麼,用戶除了重新啓動系統將沒有其他的選擇,重要的是,所有在內存中的數據也將同時丟失。

        操作系統的專家們顯然意識到這個問題,從Windows NT 開始,開始採用搶先式多任務系統,每一個運行的程序都分配在一個獨立的進程(Process)中,一個進程實際上是一個數據結構,描述運行這個程序所需資源的信息,例如內存或者堆棧的使用情況。線程(Thread)是包含在進程中的一種資源,本質上說,線程也是一個特殊的數據結構,用於描述程序執行的狀態信息,例如寄存器的狀態等。從使用的角度來看,線程就是一個虛擬的CPU,這樣,從邏輯上我們可以認爲程序運行在一個線程上。

       當然,對於單CPT的機器來說,實際上同時還是隻能運行一個程序,操作系統負責系統中哦線程在物理CPU上的切換管理。通常情況下,操作系統通過分配時間片的方式,調度系統中的各個線程,使得看起來似乎在同時運行多個程序。當時間片到期的時候,操作系統將剝奪線程的運行權,以便將CPU調度給其他的線程。這樣一個程序死掉,將不會影響到其他的程序,這種調度方式稱爲搶先式機制。對於多CPU哦系統,操作系統可以將線程分配到不同的CPU,更好地利用多CPU帶來的好處。


線程

        通過上面的分析可以看到,操作系統通過線程對程序的執行進行管理。
        當操作系統運行一個程序的時候,首先,操作系統將爲這個準備運行的程序分配一個進程,以管理這個程序所需要的各種資源。在這些資源之中,會包含一個稱爲主線程的線程數據結構,用來管理這個程序的執行狀態。
         在Windows操作系統下,線程這個數據結構將會包含以下內容:
              ×線程的核心對象,其中主要包含線程當前的寄存器狀態,當操作系統調度這個線程開始運行的時候,寄存器的狀態將被加載到CPU中,重新構建線程的執行環境,當線程被調度出來的時候,最後的寄存器狀態被重新保存到這裏,以備下一次執行的時候使用。

            ×線程環境塊(Thread environment block,TEB),是一塊用戶模式下的內存,包含線程的異常處理鏈的頭部。另外,線程的局部存儲數據(Thread Local storage
 Data)也存在這裏。

           ×用戶模式的堆棧,用戶程序的局部變量和參數傳遞所使用的堆棧,默認情況下Windows將會分配1M的空間用戶用戶模式的堆棧。

           ×內核模式堆棧,用於訪問操作系統時使用哦堆棧。

         在搶先式多任務的環境下,在一個特定的時間,CPU將一個線程調度進CPU中執行,這個線程最多將會運行一個時間片的時間長度,當時間片到期後,操作系統將這個線程調度出CPU,將另外一個線程調度進CPU,我們通常稱這種操作爲上下文切換。在每一次的上下文切換時,Winddows將執行下面的步驟:
1)將當前的CPU寄存器的值保存到當前運行的線程數據結構中,即其中的線程核心對象中。
2)選擇下一個準備運行的線程,如果這個線程處於不同的進程中,那麼,還必須首先切換虛擬地址空間。
3)加載準備運行線程的CPU寄存器狀態到CPU中。

         公共語言運行時CLR(Common) Language Runtime)是 .NET 程序運行的環境,它負責資源管理,並保證應用和底層操作系統之間必要的分離。
在 .NET環境下,CLR中的線程需要通過操作系統的線程完成實際的處理工作,目前情況下,.NET 直接將CLR中的線程映射到操作系統的線程進度處理和調度,所以我們每創建一個線程將會消耗1M以上的內存空間。但是,爲了CLR中的線程並不一定與操作系統中的線程完全對應。通過創建CLR環境下的邏輯線程,我們可能創建更加節省資源的線程,使的大量的CLR線程可以工作在少量的操作系統線程之上。

         在線程的生命的週期中,可以處於多個不同的狀態下,剛剛創建的線程處於已經準備好運行,但是還沒有運行的狀態,我們通常稱爲(Ready)準備狀態。在操作系統的調度之下,這個線程可以進入運行狀態,即Running(運行)狀態。運行狀態的線程可能因爲時間片的關係被操作系統切換出CPU,稱爲Suspended(暫停運行)狀態,也可能在時間片還沒有用完的情況下,因爲等待其他的任務,而轉換到Blocked(阻塞)狀態。在阻塞狀態下的線程,隨時可以因爲在此的調度重新進入運行狀態。線程還可以通過Sleep方法進入Sleep(睡眠)狀態,當睡眠時間到期之後,可以在此被調用運行。

        處於運行狀態的線程還可能被主動終止執行,直接結束;也可能因爲任務已經完成,被操作系統正常結束。

線程的狀態轉換關係如圖


自定義線程


        對於一個運行的程序來說,主線程將有操作系統直接創建並調度,但是,對於許多程序來說,僅僅只有一個線程顯然是不夠的。
        例如,對於圖形用戶界面(GUI)來說,絕大多數的用戶都是單線程的,因爲多線程的用戶界面將會帶來極大的效率問題。對於Windows的GUI程序來說,消息循環工作在一個線程之上,或者說,整個窗口的輸入和蔬菜都是工作在一個線程之上的,這樣的話,如果我們在某個菜單的處理中出現了一個耗費很長時間的工作,比如,調用一個計算pi的前100萬位的,那麼,在這個方法完成之前,我們的用戶界面將不會有任何響應,因爲計算過程佔用了線程,窗口沒有機會獲得用戶的操作,也沒有機會更新窗口的現實。
        那麼,上面的問題如何解決呢?
        答案是創建自定義的線程來完成計算任務,而界面的線程不要被計算任務所佔用,這樣,我們的一個線程也將擁有多個線程,這種程序也就是所謂的多線程程序,而這些自定義的線程也被稱爲自由線程。
        當然,這時就會出啊先線程的調度、線程的同步等線程管理的問題。



前臺線程和後臺線程

        對於ixiancheng我們還可以分爲兩類:前臺線程和後臺線程。
        前臺線程能夠保持程序的運行,當一個進程中所有的前臺線程結束的時候,操作系統將立即結束這個程序進程。注意,啓動程序時所創建的主線程一定是前臺線程。
         後臺線程不能保證程序的存活,這意味着當後臺線程還沒有完全完成的時候,程序也可能已經結束。當前臺線程全部結束的時候,所有的後臺線程都將被終止,而且不會有異常拋出。
         如果希望線程能夠可靠完成,應該將這個線程設置爲前臺線程。對於不關鍵的任務,可以使用後臺線程完成。在 .NET 環境下,從非託管代碼進入托管執行環境的所有線程都被標記爲後臺線程。通過創建並啓動新的Thread對象而生成的所有線程都默認爲前臺線程。
         線程對象IsBackground屬性是一個可讀寫的屬性,可以用來設置線程是前臺還是後臺線程。需要注意的是,必須在線程啓動之前進行設置,線程啓動之後就不能設置了。
         如果使用一個線程監聽程序的活動(例如套接字連接),可以將其IsBackground屬性設置爲true,這樣這個監控線程就不會阻塞程序的終止。


工作者線程和I/O線程

        對於線程所執行的任務來說,可以將線程任務分爲兩種類型:工作者(Worker)線程 和 I/O線程。
工作者線程用來完成計算密集的任務,在任務的執行過程中,需要CPU不間斷地處理,所以,在工作者線程的執行過程中,CPU和線程的資源是充分利用的。
        I/O線程典型的情況是用來完成輸入和輸出工作,在這種情況下,計算機需要通過I/O設備完成輸出和輸入任務。在處理過程中,操作系統通過計算機的硬件設備完成實際的輸入和輸出工作,CPU可以不必完全參與到輸入和輸出的處理過程中,CPU僅僅需要在任務開始的時候將任務的參數傳遞給設備,然後啓動硬件設備即可。等到任務完成的時候,CPU收到一個通知,一般來說,是一個硬件的中斷信號,此時,CPU繼續後繼的處理工作。
        從上面的描述可以看到,在處理的過程中,CPU是不必完全參與處理過程的,如果在運行的線程不交出CPU的控制權,那麼線程也只能處於等待狀態,在任務完成後纔會有事可做,此時,線程所佔用的空間還將被使用,但是並沒有CPU在使用這個線程,可能出現線程資源浪費的問題。
         如果我們的程序是一個網絡服務程序,針對每一個網絡連接都使用一個線程進行管理,那麼,此時將會出現大量哦線程都在等待網絡通信,隨着網絡連接的不斷增加,處於等待狀態哦線程將會很快消耗進所有的內存資源。
        面對這些問題可以考慮通過線程池來解決。



線程池

       在前面的分析中,我們任務線程是一個昂貴的資源,僅僅從內存的角度來說,每個線程就將佔用1M以上的內存,而且,初始化內存中的數據結構,包括在消耗線程時的處理,都更加顯得線程是一個昂貴的資源。
        針對這種情況,我們可以考慮使用少量的線程來管理大量的網絡連接,比如說,在啓動輸入輸出處理之後,只知用一個線程監控網絡通信的狀況,在這種情況下,需要進行網絡通信的線程在啓動通信開始之後,就已經可以結束了,也就是說,可以被系統回收了。在通信的傳輸階段,由於不需要CPU參與,可以沒有線程介入。監控線程將負責在信息到達之後,重新啓動一個計算密集的線程完成本地的處理工作。這樣帶來的好處就是將沒有線程處於等待狀態來消耗有效的內存資源。
        所以,對於I/O線程來說,可以將輸入輸出的操作分爲三個步驟:啓動、實際輸入輸出、處理結果。由於實際的輸入輸出可由硬件完成,並不需要CPU的參與,而啓動和處理結果也並不需要必須在同一個線程上進行,爲了提高線程的利用率,可以將輸入輸入的操作分爲兩步來進行,以便充分利用線程資源。一般來說,在.NET中啓動步驟的方法名稱以Begin作爲前綴,而處理結果的方法以End作爲前綴,這兩個方法可以運行在不同的線程上。
        爲了減少創建線程、銷燬線程所帶來的效率損失,同時也爲了能夠節約寶貴的內存,可以考慮創建一個線程池,提供線程的工廠服務,這樣,就沒有必要總是創建新的線程,而是當需要線程的時候從線程池取出一個線程,當不再使用這個線程的時候,將這個線程歸還給線程池,以方便後繼的使用。
        如果需要大量的線程完成處理工作,還可以考慮創建一個線程的消費隊列,將需要線程處理的操作根據先入先出的順序排在一個隊列中,而線程池中僅僅需要少量的線程就可以主次完成隊列的操作。線程池的處理過程如圖:

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