大家都在用并发,小编带你了解并发的背景
寫在前面
并發(fā)與操作系統(tǒng)的生命歷程息息相關(guān)。進程的出現(xiàn),使得程序狀態(tài)的保存變?yōu)楝F(xiàn)實,為進程間的切換提供了可能,實現(xiàn)了操作系統(tǒng)的并發(fā),大大提高資源利用率。雖然進程的出現(xiàn)解決了操作系統(tǒng)的并發(fā)問題,但人們對實時性又有了更高的要求。由于一個進程由若干個子任務組成,所以人們就發(fā)明了線程,讓每個線程負責一個獨立的子任務,提高程序的響應靈敏度。一個進程雖然包括多個線程,但是這些線程是共同享有進程占有的資源和地址空間的。因此,雖然多線程提高了資源利用率,保證了實時性,但同時也帶來了包括安全性、活躍性和性能等問題。總的來說,進程讓操作系統(tǒng)的并發(fā)性成為可能,而線程讓進程的內(nèi)部并發(fā)成為可能。
進程和線程的由來
操作系統(tǒng)中為什么會出現(xiàn)進程?
說起進程的由來,我們需要從操作系統(tǒng)的發(fā)展歷史談起。
也許在今天,我們無法想象在很多年以前計算機是什么樣子。我們現(xiàn)在可以用計算機來做很多事情:辦公、娛樂、上網(wǎng),但是在 計算機剛出現(xiàn)的時候,是為了解決數(shù)學計算的問題,因為很多大量的計算通過人力去完成是很耗時間和人力成本的。 在最初的時候,計算機只能接受一些特定的指令,用戶輸入一個指令,計算機就做一個操作。當用戶在思考或者輸入數(shù)據(jù)時,計算機就在等待。顯然,這樣效率會很低下,因為很多時候,計算機處于等待用戶輸入的狀態(tài)。
那么,**能不能把一系列需要操作的指令預先寫下來,形成一個清單,然后一次性交給計算機,計算機不斷地去讀取指令來進行相應的操作?**就這樣, 批處理操作系統(tǒng) 誕生了。用戶可以將需要執(zhí)行的多個程序?qū)懺诖艓?#xff0c;然后交由計算機去讀取并逐個地執(zhí)行這些程序,并將輸出結(jié)果寫到另一個磁帶上。
雖然批處理操作系統(tǒng)的誕生極大地提高了任務處理的便捷性,但是仍然存在一個很大的問題:
假如有兩個任務 A 和 B,任務A 在執(zhí)行到一半的過程中,需要讀取大量的數(shù)據(jù)輸入(I/O操作),而此時CPU只能靜靜地等待任務A讀取完數(shù)據(jù)才能繼續(xù)執(zhí)行,這樣就白白浪費了CPU資源。人們于是想,能否在 任務A 讀取數(shù)據(jù)的過程中,讓 任務B 去執(zhí)行,當 任務A 讀取完數(shù)據(jù)之后,讓 任務B 暫停,然后讓 任務A 繼續(xù)執(zhí)行?
但是這樣就有一個問題,原來每次都是一個程序在計算機里面運行,也就說內(nèi)存中始終只有一個程序的運行數(shù)據(jù)。而如果想要 任務A 執(zhí)行 I/O操作 的時候,讓 任務B 去執(zhí)行,必然內(nèi)存中要裝入多個程序,那么如何處理呢?多個程序使用的數(shù)據(jù)如何進行辨別呢?并且,當一個程序運行暫停后,后面如何恢復到它之前執(zhí)行的狀態(tài)呢?
這個時候,人們就發(fā)明了進程,用進程來對應一個程序,每個進程對應一定的內(nèi)存地址空間,并且只能使用它自己的內(nèi)存空間,各個進程間互不干擾。并且,進程保存了程序每個時刻的運行狀態(tài),這樣就為進程切換提供了可能。當進程暫停時,它會保存當前進程的狀態(tài)(比如進程標識、進程的使用的資源等),在下一次重新切換回來時,便根據(jù)之前保存的狀態(tài)進行恢復,然后繼續(xù)執(zhí)行。
這就是并發(fā),能夠讓操作系統(tǒng)從宏觀上看起來同一個時間段有多個任務在執(zhí)行。換句話說,進程讓操作系統(tǒng)的并發(fā)成為了可能。注意,雖然并發(fā)從宏觀上看有多個任務在執(zhí)行,但是事實上,任一個具體的時刻,只有一個任務在占用CPU資源(當然是對于單核CPU來說的)。
為什么會出現(xiàn)線程?
在出現(xiàn)了進程以后,操作系統(tǒng)的性能得到了大大的提升。雖然進程的出現(xiàn)解決了操作系統(tǒng)的并發(fā)問題,但是人們?nèi)匀徊粷M足,人們逐漸對 實時性 有了要求。因為一個進程在一個時間段內(nèi)只能做一件事情,如果一個進程有多個子任務,只能逐個地去執(zhí)行這些子任務。比如,對于一個監(jiān)控系統(tǒng)來說,它不僅要把圖像數(shù)據(jù)顯示在畫面上,還要與服務端進行通信獲取圖像數(shù)據(jù),還要處理人們的交互操作。如果某一個時刻該系統(tǒng)正在與服務器通信獲取圖像數(shù)據(jù),而用戶又在監(jiān)控系統(tǒng)上點擊了某個按鈕,那么該系統(tǒng)就要等待獲取完圖像數(shù)據(jù)之后才能處理用戶的操作,如果獲取圖像數(shù)據(jù)需要耗費 10s,那么用戶就只有一直等待。顯然,對于這樣的系統(tǒng),人們是無法滿足的。
**那么,可不可以將這些子任務分開執(zhí)行呢?**即,在系統(tǒng)獲取圖像數(shù)據(jù)的同時,如果用戶點擊了某個按鈕,則會暫停獲取圖像數(shù)據(jù),而先去響應用戶的操作(因為用戶的操作往往執(zhí)行時間很短),在處理完用戶操作之后,再繼續(xù)獲取圖像數(shù)據(jù)。人們就發(fā)明了線程,讓一個線程去執(zhí)行一個子任務,這樣一個進程就包括了多個線程,每個線程負責一個獨立的子任務。這樣,在用戶點擊按鈕的時候,就可以暫停獲取圖像數(shù)據(jù)的線程,讓 UI線程 響應用戶的操作,響應完之后再切換回來,讓獲取圖像的線程得到 CPU資源 。從而,讓用戶感覺系統(tǒng)是同時在做多件事情的,滿足了用戶對實時性的要求。
換句話說,進程讓操作系統(tǒng)的并發(fā)性成為可能,而線程讓進程的內(nèi)部并發(fā)成為可能。但是要注意,一個進程雖然包括多個線程,但是這些線程是共同享有進程占有的資源和地址空間的。進程 是操作系統(tǒng)進行資源分配的基本單位,而 線程 是操作系統(tǒng)進行調(diào)度的基本單位。
多線程并發(fā)
由于多個線程是共同占有所屬進程的資源和地址空間的,那么就會存在一個問題:如果多個線程要同時訪問某個資源,怎么處理? 這個問題就是并發(fā)安全性問題。
此外,可能有朋友會問,現(xiàn)在很多時候都采用多線程編程,那么是不是多線程的性能一定就由于單線程呢?答案是不一定,要看具體的任務以及計算機的配置。比如說:對于單核CPU,如果是 CPU密集型任務,如解壓文件,多線程的性能反而不如單線程性能,因為解壓文件需要一直占用 CPU資源,如果采用多線程,線程切換導致的開銷反而會讓性能下降。但是對于比如交互類型的任務,肯定是需要使用多線程的。而對于多核CPU,對于解壓文件來說,多線程肯定優(yōu)于單線程,因為多個線程能夠更加充分利用每個核的資源。
雖然多線程能夠提升程序性能,但是相對于單線程來說,它的編程要復雜地多,要考慮線程安全問題。因此,在實際編程過程中,要根據(jù)實際情況具體選擇。
并發(fā)簡史總結(jié)
- 早期的計算機不包含操作系統(tǒng),它們從頭到尾只執(zhí)行一個程序,并且這個程序能夠訪問計算機中的所有資源。這對于昂貴且稀有的計算機資源來說是一種浪費;
- 操作系統(tǒng)的出現(xiàn)使得計算機能同時運行多個程序,不同的程序都在單獨的進程中運行,并且操作系統(tǒng)為各個獨立執(zhí)行的進程分配資源( eg: 通過粗粒度時間分片使程序共享資源,如 CPU 等 )。這無疑提高了計算機資源的利用率;
- 在早期的分時系統(tǒng)中,每個進程的執(zhí)行都是串行的。串行編程模型的優(yōu)勢在于其簡單性和直觀性,因為它每次只做一件事情,做完之后再做另一件。這種串行編程模型仍然存在著計算機資源利用率不高的問題;
- 促使進程出現(xiàn)的因素同樣也促使著線程的出現(xiàn)。線程允許在同一個進程中同時存在多個程序控制流。線程會共享進程范圍內(nèi)的資源,但每個線程都有各自的 程序計數(shù)器 、 棧 以及 局部變量 等等;
- 線程也被成為輕量級進程。在大多數(shù)現(xiàn)代操作系統(tǒng)中,都是以線程為基本的調(diào)度單位,而不是進程。如果沒有明確的協(xié)同機制,那么線程將彼此獨立執(zhí)行。由于同一個進程的所有線程都將共享進程的內(nèi)存地址空間,因此這些線程都能訪問相同的變量,這就需要實現(xiàn)一種比進程間共享數(shù)據(jù)粒度更細的數(shù)據(jù)共享機制。如果沒有明確的同步機制來協(xié)同對共享數(shù)據(jù)的訪問,將造成不可預測的結(jié)果。
線程的優(yōu)勢
解耦、簡化程序開發(fā)
在程序中,如果我們?yōu)槊糠N類型的任務都分配一個專門的線程,那么可以形成一種串行執(zhí)行的假象,并將程序的執(zhí)行邏輯與調(diào)度機制的細節(jié),交替執(zhí)行的操作,異步 I/O 以及資源等待等問題分離開來。通過使用線程,可以將復雜并且異步的工作流進一步分解為一組簡單并且同步的工作流,每個工作流在一個單獨的線程中運行,并在特定的同步位置進行交互。
Servlet 框架就是一個很好的例子。框架負責解決一些細節(jié)問題,包括請求管理、線程創(chuàng)建、負載平衡等,并在正確的時刻將請求分發(fā)給正確的應用程序組件(對應的一個具體Servlet)。編寫 Servlet 的開發(fā)人員不需要了解有多少請求在同一時刻被處理,也不需要了解套接字的輸入(出)流是否被阻塞。當調(diào)用 Servlet 的 service 方法來響應 Web請求時,可以以同步方式來處理這個請求,就好像它是一個單線程的程序。這種方式簡化了組件的開發(fā),大大降低框架學習門檻。
多線程還有助于用戶界面的靈敏響應。例如,在 Android 開發(fā)中,我們常常將網(wǎng)路請求或 I/O 等耗時操作單獨放到一個線程中,以提高響應的靈敏度。
提高資源利用率
多處理器系統(tǒng)的出現(xiàn),使得同一個程序的多個線程可以被同時調(diào)度到多個 CPU 上運行。因此,多線程程序可以通過提高處理器資源的利用率來提升系統(tǒng)的吞吐率。其實,多線程程序也有助于在單處理器系統(tǒng)上獲得更高的吞吐率(如果程序的一個線程在等待 I/O 操作的完成,另一個線程可以繼續(xù)運行,使程序能夠在 I/O 阻塞期間繼續(xù)運行)。
線程帶來的風險
安全性問題
在線程安全性的定義中,最核心的概念就是正確性。當多個線程訪問某個類時,不管運行時環(huán)境采用何種調(diào)度方式或者這些線程將如何交替執(zhí)行,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同,這個類都能表現(xiàn)出正確的行為,那么這個類就是線程安全的。
線程不安全類示例:
@NotThreadSafe public class UnsafeSequence { private int value;/** Returns a unique value. */public int getNext() { return value++; } } 復制代碼雖然 遞增運算 “value++” 看上去是單個操作,但實際上它包含三個獨立的操作:讀取 value, 將 value 加 1,并將計算結(jié)果寫入 value。由于運行時各個線程執(zhí)行順序的不確定性,可能這段代碼在不同線程的調(diào)用中返回相同的數(shù)值
活躍性問題
活躍性問題關(guān)注的是:某件正確的事情最終會發(fā)生。導致活躍性的問題包括死鎖、饑餓等。
性能問題
性能問題關(guān)注的是:正確的事情能夠盡快發(fā)生。性能問題包括多個方面,例如響應不靈敏,吞吐率過低,資源消耗過高等。在多線程程序中,當線程調(diào)度器臨時掛起活躍線程并轉(zhuǎn)而運行另一個線程時,就會頻繁出現(xiàn)上下文切換操作(Context Switch),這種操作會導致 CPU 時間更多的花在線程調(diào)度上而非線程的運行上。
線程無處不在
在 Java 中,一個應用程序?qū)粋€JVM實例(JVM進程)。Java采用的是 單線程編程模型 ,即在我們自己的程序中如果沒有主動創(chuàng)建線程的話,只會創(chuàng)建一個線程,通常稱為主線程。但是要注意,雖然只有一個線程來執(zhí)行任務,不代表JVM中只有一個線程,JVM實例在創(chuàng)建的時候,同時會創(chuàng)建很多其他的線程(比如垃圾收集器線程)。由于Java采用的是單線程編程模型,因此在進行UI編程時要注意將耗時的操作放在子線程中進行,以避免阻塞主線程(在UI編程時,主線程即UI線程,用來處理用戶的交互事件)。
public class Test {public static void main(String[] args) {// 獲取運行當前代碼的線程的名字String curThreadName = Thread.currentThread().getName(); System.out.println(curThreadName);} } 復制代碼小結(jié)
轉(zhuǎn)載于:https://juejin.im/post/5cb5db1f6fb9a0688a680a7d
總結(jié)
以上是生活随笔為你收集整理的大家都在用并发,小编带你了解并发的背景的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python学习笔记之六:在VS中调用P
- 下一篇: java并发编程线程安全