同步阻塞、同步非阻塞、异步阻塞、异步非阻塞与 I/O 多路复用、Java NIO 之间的联系
同步阻塞、同步非阻塞、異步阻塞、異步非阻塞與 I/O 多路復用、Java NIO 之間的聯系
- 先驗知識
- 此處的異步指的是什么
- 同步、異步、阻塞、非阻塞
- 同步阻塞、同步非阻塞、異步阻塞、異步非阻塞
- 一個生動的例子
- I/O 多路復用
- 總結與補充
先驗知識
??在解釋這幾個概念之前,需要注意的是:
-
首先需要知道操作系統層面的同步、異步、阻塞這幾個概念的含義。關于這方面的內容,可見筆者的另一篇博客:
同步與異步、并行與并發、阻塞與掛起:
https://blog.csdn.net/wangpaiblog/article/details/116114098 -
本文解釋的概念至少適用于編程語言層面,但不適用于操作系統層面。原因是,在軟件工程中,任何設計都可以進行分層封裝。其中,每個中間層的設計對上層與下層來說都是透明的。因此,編程語言層面的“同步、異步、阻塞”與操作系統層面的沒有必然聯系。
-
有人喜歡對“同步阻塞、同步非阻塞、異步阻塞、異步非阻塞”的概念,首先區分發送方和接收方,但實際上,無論是發送還是接收,本質上都是一種“請求”的過程,如果不在操作系統層面進行分析,區分這兩者實無必要。
-
同步、異步是針對兩個任務(此任務不同于操作系統理論中的作業、進程等,指的只是一個線程中要做的一件事情。本文中的任務都指的是這個。)來說的,其中一個任務為一開始就在執行、所主要關注的,稱為主任務,而另一個是與主任務相關的一個任務,稱為相關任務。當主任務與相關任務分別位于不同的線程時,稱該線程分別為主線程、相關線程。
-
阻塞是針對主任務所在的線程來說的。當主任務和相關任務位于同一個線程時,不存在“阻塞”,即此情況下不存在同步阻塞、異步阻塞。
但對于下面的情況,主任務和相關任務一定位于不同的線程:
-
主任務發起的是 I/O 請求
-
主任務發起的是網絡請求
-
-
請求是一個期望獲得資源的行為,而“同步阻塞、同步非阻塞、異步阻塞、異步非阻塞”描述的是獲得期望資源之前的行為,而不是其之后的行為。這四個概念中的“阻塞”不描述在獲得資源之后主線程的程序走向。
此處的異步指的是什么
??首先需要明白,無論是對“異步”還是“非阻塞”的主線程,都不可能憑空接收到外界、執行沒有事先設置的程序。而之所以主線程的行為會受到相關線程的影響,是因為主線程會周期性地調用一種“請求”函數,而此“請求”函數的行為會受相關線程的影響。
??其次,對于操作系統,只要其啟動,就一直在運行程序。如果沒有顯式的進程,就運行一個默認的空進程。另外,編程人員不可能事先預計用戶本次對本軟件的使用時長,然后在軟件中設置一個運行時間與其正好相等的一種任務。這就是說,線程在執行時,實際上是在無限循環、周期性地執行一系列任務。于是,可以在每個任務之間插入一個中斷點,用于執行額外的操作(任務),以實現對其它線程的交互,這就是異步操作。理由是,中斷點之前的任務都已經完成,而任務之間本是不應有任務的(因為任務之間的部分屬于空檔,沒有任何東西),因此,任務之間的中斷點是由中斷點之前的某任務插入的。由于某種原因,該任務并不想馬上執行某操作,而是選擇將其移至一系列任務執行完之后的某個任務之間的空檔來執行,因此稱為異步操作。
同步、異步、阻塞、非阻塞
??前面已經指出了關鍵性的知識點,下面將直接給“同步阻塞、同步非阻塞、異步阻塞、異步非阻塞”的概念。為了更好的說明,筆者做了一張圖,如下:
??對于主線程上執行的一系列任務,如果其中的某個任務需要與相關線程交互,當其立即暫時當前的任務而發起請求,這稱為同步;當該任務將請求的時間安排到某任務完成之后再發起,這稱為異步。當發起請求時,直至獲得完整的資源之后,不會繼續執行現在或之后的任務,這稱為阻塞;當發起請求時,立即獲得瞬時的結果,然后繼續執行現在或之后的任務,如果獲得的瞬時資源不是完整的資源,將之后周期性發送類似的請求,直至獲得完整的資源,這稱為非阻塞。
??可以看出,同步與異步的區別在于發起請求的時機,而阻塞與非阻塞的區別在于發起請求后是否對本線程進行暫停。
同步阻塞、同步非阻塞、異步阻塞、異步非阻塞
??很多讀者(包含筆者)都喜歡作者能直截了當地給出概念,而反感拐彎抹角和旁敲側擊。因此,筆者再提煉一下本文核心的四個概念:
-
同步阻塞:在需要某資源時馬上發起請求,并暫停本線程之后的程序,直至獲得所需的資源。
-
同步非阻塞:在需要某資源時馬上發起請求,且可以馬上得到答復,然后繼續執行之后的程序。但如果得到的不是完整的資源,之后將周期性地的請求。
-
異步阻塞:在需要某資源時不馬上發起請求,而安排一個以后的時間再發起請求。當到了那時發出請求時,將暫停本線程之后的程序,直至獲得所需的資源。
-
異步非阻塞:在需要某資源時不馬上發起請求,而安排一個以后的時間再發起請求。當到了那時發出請求時,可以馬上得到答復,然后繼續執行之后的程序。但如果得到的不是完整的資源,之后將周期性地的請求。
【提醒】
??有些過分鉆研概念的極客可能會對此提出質疑:既然“不馬上發起請求”屬于異步,但是如果一個線程將 CPU 分配的時間片用完了,此時應該得不到執行,那么同步非阻塞是不是可以歸于異步非阻塞呢?
??實際上不是這樣。一方面,這里的“馬上執行”,指的是針對這個程序中相關的代碼而言的。基于上一條代碼而執行的下一條代碼就稱為“被馬上執行的代碼”,而不管這兩條代碼的執行之間有沒有被 CPU 中斷。另一方面,評判 同步非阻塞 與 異步阻塞 的其中一個標準是,如果一個主線程中,依次調用了 N 個 同步非阻塞 的方法,則這些方法被執行的次序是不確定的。但如果一個主線程中,依次調用了 N 個 異步阻塞 的方法,則當主線程完成當前的任務之后,轉而真正執行這些異步方法時,這些方法被執行的次序與之前調用時一致。
一個生動的例子
??一個貼切生動的例子可能對理解更有幫助。這里假設了這樣的一種情景:筆者正在進行公司安排的一個“cleancode”專項需求(下面簡稱 cleancode 專項),然后突然對于門禁上報告的一項告警不太理解,筆者想要求助自己的同事(設該同事名為 Bob),于是在公司的通信軟件(設該軟件名為 contact)上向其發送了此求助消息,并假設筆者每天有減脂的訴求,因此在下午下班后不會立刻去吃飯。
??前述的四個概念類比如下:
-
同步阻塞:筆者在 contact 上給 Bob 發了一條咨詢信息,并開啟 contact 的消息自動彈出功能。然后筆者暫停手頭的工作,翹著二郎腿開始用手機摸魚,直到手機上彈出 contact 的關于 Bob 的回復信息。
解釋:
-
筆者、筆者的同事 Bob:兩個位于不同服務器上的操作系統。
-
cleancode 專項:正在“筆者操作系統”上運行的一個線程。
-
筆者放棄工作上的任務:cleancode 專項線程被阻塞。
-
筆者開始摸魚:cleancode 專項線程因阻塞而使筆者空閑,筆者通過調度來運行其它無關線程。
-
contact 的消息自動彈出:在操作系統中,用于喚醒阻塞線程的信號量。
-
-
同步非阻塞:筆者在 contact 上給 Bob 發了一條咨詢信息,然后筆者繼續做 cleancode 專項中的其它內容,并周期性查看 Bob 有沒有回復。
-
異步阻塞:筆者在日程表上記錄了這個待辦事項,然后筆者繼續做 cleancode 專項中的其它內容,最后到下午下班時,筆者在 contact 上給 Bob 發了一條咨詢信息,并開啟 contact 的消息自動彈出功能。然后筆者暫停手頭的工作,翹著二郎腿開始在晚上加班時間用手機摸魚,直到手機上彈出 contact 的關于 Bob 的回復信息。
-
異步非阻塞:筆者在日程表上記錄了這個待辦事項,然后筆者繼續做 cleancode 專項中的其它內容,最后到下午下班時,筆者在 contact 上給 Bob 發了一條咨詢信息,然后筆者繼續加班做 cleancode 專項中的其它內容,并周期性查看 Bob 有沒有回復。
I/O 多路復用
說明:
??在本小節 I/O 多路復用 中,當名詞 同步、異步 單獨使用時,它指的是前面 先驗知識 中提到的“操作系統層面的同步、異步”,而不是 同步阻塞、同步非阻塞、異步阻塞、異步非阻塞 中的 “同步、異步”。
??同步非阻塞相對于同步阻塞已經有很大的提高了,不過它依然有嚴重的性能問題。首先,在多線程中,不管它們之間的交互使用的是 同步阻塞、同步非阻塞、異步阻塞、異步非阻塞 中的哪種,它們之間都要解決 同步 的問題。對于同步非阻塞,由于它是非阻塞的,所以它需要不斷地詢問對方的狀態,這會占用 CPU 的時間。如果有很多線程都選擇這么做,系統的性能將受到很大影響。
??有一些公共資源請求是經常發生的,比如 I/O 請求。如果使用同步非阻塞的方式來請求,一個問題就是,這些線程彼此之間互不了解對方的情況,在發出請求時沒有全局意識。比方說,如果一個臨界資源有很多線程都在請求,在這種情況下,應該會有很多線程一定請求不到。但是它們并不知道這一點,因此仍然發出這種請求,這顯然是一種浪費行為。那既然這個需求比較熱門,那么能不能類似于線程池一樣,尋找一個管理公共資源的中介者來協調這種資源的分配呢?
??答案是可以的。可以構建一個中介者來接收所有的請求,然后將這些資源按一定的算法來為這些請求作分配。這實際上是一種 Reactor 模式。由于使用了一個中介者來管理這些請求,因此發出這個請求的請求方就不需要周期性地查詢資源的狀態,它可以交給中介者一個回調方法來令其幫忙實現同步。不過,因為不進行周期性地查詢,所以這是一種阻塞查詢。另外,對請求者來說,因為資源是由中介者來分配,其不能保證何時調用這個回調同步方法,所以這個過程是一種異步阻塞請求。
??I/O 多路復用(I/O multiplexing)正是這樣的一種模式。它使用中介者來接收所有的請求,并輪詢和管理相應的 I/O 資源。因此,I/O 多路復用是一種異步阻塞 I/O。Java 中的 NIO 正是基于 I/O 多路復用技術。Java NIO 中的 N,指的是 New,而不是 Non-blocking(非阻塞)。很多人分不清這一點,因為他們把異步與非阻塞混為一談。
總結與補充
-
用一句話概括本文的概念:
-
異步:把事情推到以后去做
-
阻塞:專心做一件事情
-
同步非阻塞:一邊做一件事情,一邊做另一件事情(一心二用)
-
異步阻塞:把問題推到以后專心處理
-
同步阻塞:馬上專心做一件事情
-
異步非阻塞:把問題推到以后時不時處理一下
-
-
線程在被阻塞時,CPU 會將時間片分給其它線程。而當線程發出非阻塞線程請求時,它以后還要周期性地請求,這同樣會占用 CPU 時間。因此,不能一味地認為異步非阻塞一定優于同步非阻塞。同樣是異步非阻塞,底層實現不同,效率也不同。將程序中的所有代碼都改成異步非阻塞,也未必可以提高系統的整體性能。哪種性能最好要取決于具體實現,而不是幾個用于裝蒜的術語。
-
Java 中沒有異步關鍵字,所以一般情況下,Java 代碼都是同步的,Java 中只有同步阻塞和同步非阻塞。但異步代碼可以通過同步代碼設計出來,所以 Java 中也可以設計出異步方法。
JavaScript 有異步關鍵字,但 JavaScript 是單線程的,所以 JavaScript 中只有同步阻塞和異步阻塞。
-
我們平常單獨使用的“同步”一詞,實際上指的是這里的同步阻塞,而“異步”指異步非阻塞。
-
在操作系統層面,只有單獨的同步與異步、阻塞與非阻塞的說法。此時的同步與異步的含義主要有以下幾個:
-
同本文的同步阻塞、異步非阻塞。
-
同步:強調兩個程序的運行彼此有邏輯、時間上的先后關系。
異步:強調兩個程序的運行彼此相對獨立。
-
總結
以上是生活随笔為你收集整理的同步阻塞、同步非阻塞、异步阻塞、异步非阻塞与 I/O 多路复用、Java NIO 之间的联系的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用 Redis 实现分布式锁(Java
- 下一篇: Java 函数式编程入门