你真的理解計算機編程的進程和線程的使用嗎?

你真的理解計算機編程的進程和線程的使用嗎?

前言

前面我寫過一些文章介紹了Java高級編程的一些基礎知識,在這些內容中有一個非常重要的基礎點就是線程執行問題。如何在編程中使用線程的概念,以及線程之間的通信與關聯問題。這裡我不再去重複概念的描述,只想說一下我個人對它的理解。

進程與線程

熟悉操作系統的人都知道,我們計算機上每一個應用的運行都是以進程的方式來完成的。而從操作系統角度來說,它是有一個個運行於硬件上的代碼程序構成,它又是其它更高級應用程序運行的基礎。一般應用程序運行的進程在某種程度上可以看做是操作系統的線程。

線程是我們代碼運行的一個路徑,一段程序可能有多條路徑存在,但是它都屬於一個進程,即可執行的程序。

從硬件的角度上說,每一個程序的運行都是基於CPU的運算和內部存儲來支持的,所以從這個角度上我們可以把進程理解為計算機為該程序運行分配的地址空間以及該程序執行時的邏輯控制線程。

當然我們知道絕大部分程序都不是簡單到只需要一個控制線程就能夠完成的,而能夠讓程序得以運行的基礎只有一個核心就是CPU,而CPU和它的寄存器為我們提供了能夠自由開創獨立執行空間和控制器的基礎空間,我們通常編寫的應用程序可能有多個控制線程來管理執行內容。也就是說會涉及到它們對對多個操作數據的地址空間的管理。

仔細觀察我們就知道,我們寫程序的每一段代碼其實都是一個函數,它需要執行的入口,它接收輸入,它提供執行的內容,最後返回結果。

如此,我們就需要有一個獨立的線程來對這一過程進行控制,知道入口在哪裡,知道輸入參數,然後執行的函數體,最後將返回結果寫入到哪裡。

同樣當我們進入一段代碼時,代碼本身可能也需要有自己的獨立空間來存儲只屬於它本身的參數,代碼和返回值。如此這就需要我們開啟另一個線程和對應的控制器,我們CPU內部結構裡是通過數據寄存器來實現的。

我們可以這樣理解一段代碼或者程序,它必須有一個入口,和一個出口或者結束點。每個點都會定義一定長度的內存空間作為其標識。

讓CPU知道它可以從哪個地址空間裡獲取輸入參數,執行哪些地址裡的運算表達式,然後將結果釋放到那個地址的返回值空間裡。

你真的理解計算機編程的進程和線程的使用嗎?

多線程程序結構

如果我們應用程序有過個線程同時存在,並且他們的執行彼此之間需要通信,我們都知道一般語言的內存操作模型都是將使用公共的地址來存取共用的數據然後每個執行的線程都會有自己獨立的棧空間來完成運算,並且運算過程中它們會將公共的數據拷貝到自己的棧空間內部進行操作,操作完成後會在退出自己的運行棧時區覆蓋主內存中的公共數據。

線程可以簡單的看做是一段執行態的代碼。它一般通過棧操作來進行,也就是說CPU通過後進先出一步步將要執行的代碼讀入棧中,並將一些需要實例化的對象在堆中存儲然後將其指針壓入棧中,以此來控制CPU可以操作的數據地址,因為棧是後進先出模式的,可以有效的實現過程語句的順序執行。

Java有一個成熟的內存計算模型,JMM,它詳細的定義了線程執行的過程和機制以及對於共享數據爭用時的處理規範。可以看一下我之前寫的相關文章。

一段代碼進入可執行狀態就是被讀入寄存器等待CPU指令的執行,這就是我們常見的Runnable規範定義。要求必須有一個獨立的入口函數,沒有返回值基本固定定義好函數名為run(),就如同我們每個進程級可執行程序必須有一個靜態的main()方法一樣。

所以我們在定義線程的抽象模型時,也設定了它可調用的方法名字,run(),我們要做編程過程中開始一個新的線程,必須讓運行的代碼是包含一個入口函數run()方法實現的。它會告訴操作系統需要創建一個獨立的執行棧,將所需的內容都按照執行順序壓入其中,然後從上到下開始逐步執行。

當然當我們需要一個線程執行完畢後返回一個結果時,我們需要在其開始的時候壓入一個結果地址在棧底,代碼執行結束後將結果拷貝到指定的地址空間裡。完成返回結果值操作。

Java中Callable接口定義了一個返回值,其類型可採用泛型規定,在編譯時編譯器會根據運算情況推測出結果類型並替換泛型符號。

總得來說我們可以將我們編寫的一個完整程序的過程理解為一條從某一個源點開始流淌的大河,中間它會不斷的分叉,產生支流,甚至迴旋往復,但最後都會從一個入海口流入大海。

源頭和入海口都只有一個的情況下,我們可以忽略這條大河流淌過程中有多支流和迴旋甚至池湖,這就如同我們編寫的每個函數一樣有入口,有出口,中間就是表現業務邏輯的內容。我們每條幹流或者支流都是一個線程,或者每個線程內部還會有多個獨立線程構成,它們之間可能有千絲萬縷的聯繫,但是每個線程的執行都是獨立的,都是CPU來負責。

你真的理解計算機編程的進程和線程的使用嗎?

Java程序結構

對於Java編程來說,同樣的所有可執行的程序進程都是有一個固定模式的靜態入口函數方法:

void main(String[] args) 

該方法實際上是會調用Java虛擬機來創建一個應用程序的內存運行的內存空間和主函數計數器,至於我們在該應用程序內部做如何處理,可以是簡單的順序執行的語句代碼羅列,也可以是定義方法指針的調用,將具體的邏輯處理內容定義為方法實現體。

這裡要注意的是我們在方法體內定義的任何變量都會是主函數空間內的局部變量,但是當我們想並行處理某個變量時,就需要在該主線程空間內部定義一個新的子線程,每個子線程也會有一個屬於自己的堆棧空間,它會將需要操作的主線程空間的變量拷貝到自己的線程空間內部進行操作處理,在離開子線程或者其他特定要求下會將局部變量裡對應的拷貝數據覆蓋回主線程數據。

如果我們的應用程序以獨立子線程的形式在一個多核處理上運行,就有可能出現多個並行的線程同時去更新主線程中的變量問題,這就是數據競態問題。前面我們說過通常我們會採用為線程操作排隊的方式來處理,即Synchronized同步關鍵字,或者在主線程中對於共享的變量聲明為volitile易揮發性,也就是它要求每次一個線程對其副本的修改都要立刻更新到主線程共享變量中,而不是等到子線程結束或者具體要求時才更新。

我們在通常的編程中有一個原則就是儘量的避免使用共享的變量,為了能夠獲得更高的安全性和更好的執行效率,我們推薦儘量多使用不可更改的變量,即儘可能的多用final關鍵字聲明變量。如此可以保證我們的應用程序將數據爭用的問題降到最低。

你真的理解計算機編程的進程和線程的使用嗎?

線程的使用

Java中對於線程的描述使用了Thread類,它定義了一個應用程序能夠對系統線程可以做到的所有操作接口描述。

對於一個線程來說,它在CPU上執行時絕對是對立唯一的,因為每次都會在自己的堆棧中執行。

但是我們實際的編程時,特別是現代計算機發展到了多核CPU時代,為了儘可能的利用多核的優勢,提高程序效率,會充分利用多核並行執行,如此就要求我們儘量的將可以並行的內容定義成一個可執行的代碼段。

通常我們用一個Thread類來管理執行,這個代碼段設計要求跟其它部分關聯度比較低,不會設計大量的資源爭用問題,如此可以從主線程中繁殖出多個能夠獨立執行的子線程來,在沒有資源爭用的前提下來使用不同的CPU內核並行處理。

我們可以手動的通過實現繼承Thread類或者實現Runnable或者Callable等將可以並行執行的代碼包裹起來,採用線程調度和排序等規則來管理器並行執行和對共享數據的訪問。

當然,Java平臺已經提供了很多更加複雜的線程執行類或者接口,它們通常被稱為線程池實現,允許我們方便的去定義線程並調動執行它們。

在這裡我不想一一介紹了,只是想說一定要理解它們之間的包含於被包含的關係,以及每個線程都有自己的獨立堆棧空間作為自己的作用區域,凡是位於父線程空間的內容,都可以被子線程訪問,這也是Java線程間通信最重要的一種方式。

當然,還有另外一種就是事件通知模式,它是將每個線程的發生都定義成一個事件發送出來,由其它對該事件感興趣的線程獲取事件對象,並對事件進行處理,已達到線程間通信的目的。

比如,在我們實際的應用程序開發過程中,我們可能會遇見很多比如需要大量彼此孤立有密集的計算和處理過程的處理,並且需要在最後需要將所有的處理結果進行合併處理。這種情況我們可以採用ForkJoinPool來完成。它是線程池實現的一種,是一個執行特殊任務的線程池,命名為ForkJoinTask。

它提供了一個對接口Future的實現,使得我們可以對該線程處理的過程進行管理,可以調用其get()、cancel()和isDone()等方法來掌控線程執行。

除此之外,這個類還提供了兩個方法,它們為整個框架提供了名稱:fork()和join()。

我們可以調用fork()將啟動新任務子線程並異步執行,而調用join()將等待任務子線程完成並檢索其結果。

藉此,我們可以將一個給定的任務分解成多個較小的任務,派生每個任務,最後等待所有任務完成。

這樣做可以使得複雜問題的實現更加容易。在計算機科學中,這種方法也稱為分治法。

每當一個問題太複雜而不能一次解決時,它就被分解成多個更小、更容易解決的問題。

你真的理解計算機編程的進程和線程的使用嗎?

總結

我們的編程其實就是對CPU的調度和內存空間的操作管理,而這些都是通過定義了一個抽象概念線程來描述的,整個Java語言的運行環境就是一個對操作系統管理的內存空間和CPU調度的一種使用,我們編寫的所有Java程序都是運行在JRE環境中的,而JRE其實就是操作系統中一個進程,這個進程描述了一個空間,在這個空間內部,所有的Java代碼被處理和執行,而龐大的代碼為了完成某種邏輯的實現需要通過更小一級的相對獨立的進程來完成,而該進程運行的環境是JRE提供的,我們的CPU每次執行的代碼又是以線程為單位,有其獨立的堆棧來管理的。

要學好編程必須理解好CPU和內存空間操作層面的概念和關係,計算機內核其實就是一個空間和調度的算法實現,高級語言的編程就是在規定內存空間內對CPU調度操作。


分享到:


相關文章: