【計算機內功心法】五:從小白到高手,你需要理解同步與非同步

碼農的荒島求生發表於2021-02-02

在這篇文章中我們來討論一下到底什麼是同步,什麼是非同步,以及在程式設計中這兩個概念到底意味著什麼,這些是進一步掌握高效能、高併發技術的基礎,因此非常關鍵。

相信很多同學遇到同步非同步這兩個詞的時候大腦瞬間就像紅綠燈失靈的十字路口一樣陷入一片懵逼的狀態:

mengbi
mengbi

是的,這兩個看上去很像實際上也很像的詞彙給博主造成過很大的困擾,這兩個詞背後所代表的含義到底是什麼呢?

我們先從工作場景講起。

苦逼程式設計師

假設現在老闆分配給了你一個很緊急並且很重要的任務,讓你下班前必須寫完(萬惡的資本主義)。為了督促進度,老闆搬了個椅子坐在一邊盯著你寫程式碼。

你心裡肯定已經罵上了“WTF,你有這麼閒嗎?盯著老子,你就不能去幹點其他事情嗎?”

老闆彷彿接收到了你的腦電波一樣:“我就在這等著,你寫完前我哪也不去,廁所也不去”

1600911423466
1600911423466

這個例子中老闆交給你任務後就一直等待什麼都不做直到你寫完,這個場景就是所謂的同步。

第二天,老闆又交給了你一項任務。

不過這次就沒那麼著急啦,這次老闆輕描淡寫“小夥子可以啊,不錯不錯,你再努力幹一年,明年我就財務自由了,今天的這個任務不著急,你寫完告訴我一聲就行”。

這次老闆沒有盯著你寫程式碼而是轉身刷視訊去了,你寫完後簡單的和老闆報告了一聲“我寫完了”。

1600911037338
1600911037338

這個例子老闆交代完任務就去忙其它事情,你完成任務後簡單的告訴老闆任務完成,這就是所謂的非同步。

值得注意的是,在非同步這種場景下重點是在你寫程式碼的同時老闆在自己刷劇,這兩件事在同時進行因此這就是為什麼一般來說非同步比同步高效的本質所在,不管同步非同步應用在什麼場景下。

因此,我們可以看到同步這個詞往往和任務的“依賴”、“關聯”、“等待”等關鍵詞相關,而非同步往往和任務的“不依賴”,“無關聯”,“無需等待”,“同時發生”等關鍵詞相關。

By the way,如果遇到一個在身後盯著你寫程式碼的老闆,三十六計走為上策。

打電話與發郵件

作為一名苦逼的程式設計師是不能只顧埋頭搬磚的,平時工作中的溝通免除不了,其中一種高效的溝通方式是吵架。。。啊不,是電話。

email
email

通常打電話時都是一個人在說另一個人聽,一個人在說的時候另一個人等待,等另一個人說完後再接著說,因此在這個場景中你可以看到,“依賴”、“關聯”、“等待”這些關鍵詞出現了,因此打電話這種溝通方式就是所謂的同步。

1600923556187
1600923556187

另一種碼農常用的溝通方式是郵件。

郵件是另一種必不可少溝通方式,因為沒有人傻等著你寫郵件什麼都不做,因此你可以慢慢悠悠的寫,當你在寫郵件時收件人可以去做一些像摸摸魚啊、上個廁所、和同時抱怨一下為什麼十一假期不放兩週之類有意義的事情。

同時當你寫完郵件發出去後也不需要乾巴巴的等著對方什麼都不做,你也可以做一些像摸魚之類這樣有意義的事情。

1600923768618
1600923768618

在這裡,你寫郵件別人摸魚,這兩件事又在同時進行,收件人和發件人都不需要相互等待,發件人寫完郵件的時候簡單的點個傳送就可以了,收件人收到後就可以閱讀啦,收件人和發件人不需要相互依賴、不需要相互等待。

你看,在這個場景下“不依賴”,“無關聯”,“無需等待”這些關鍵詞就出現了,因此郵件這種溝通方式就是非同步的。

同步呼叫

現在終於回到程式設計的主題啦。

既然現在我們已經理解了同步與非同步在各種場景下的意義(I hope so),那麼對於程式設計師來說該怎樣理解同步與非同步呢?

我們先說同步呼叫,這是程式設計師最熟悉的場景。

一般的函式呼叫都是同步的,就像這樣:

funcA() {
    // 等待函式funcB執行完成
    funcB();

    // 繼續接下來的流程
}

funcA呼叫funcB,那麼在funcB執行完前,funcA中的後續程式碼都不會被執行,也就是說funcA必須等待funcB執行完成,就像這樣:

1600925448485
1600925448485

從上圖中我們可以看到,在funcB執行期間funcA什麼都做不了,這就是典型的同步。

注意,一般來說,像這種同步呼叫,funcA和funcB是執行在同一個執行緒中的,這是最為常見的情況。

但值得注意的是,即使執行在兩個不能執行緒中的函式也可以進行同步呼叫,像我們進行IO操作時實際上底層是通過系統呼叫(關於系統呼叫請參考《程式設計師應如何理解系統呼叫》)的方式向作業系統發出請求的,比如磁碟檔案讀取:

read(file, buf);

這就是我們在《讀取檔案時,程式經歷了什麼》中描述的阻塞式I/O,在read函式返回前程式是無法繼續向前推進的

read(file, buf);
// 程式暫停執行,
// 等待檔案讀取完成後繼續執行

如圖所示:

1600925867319
1600925867319

只有當read函式返回後程式才可以被繼續執行。

當然,這也是同步呼叫,但是和上面的同步呼叫不同的是,函式和被調函式執行在不同的執行緒中。

因此我們可以得出結論,同步呼叫和函式與被調函式是否執行在同一個執行緒是沒有關係的

在這裡我們還要再次強調,同步方式下函式和被調函式無法同時進行。

同步程式設計對程式設計師來說是最自然最容易理解的。

但容易理解的代價就是在一些場景下,注意,是在某些場景不是所有場景哦,同步並不是高效的,因為任務沒有辦法同時進行。

接下來我們看非同步呼叫。

非同步呼叫

有同步呼叫就有非同步呼叫。

關於重要的非同步呼叫,你可以參考這裡

同步 vs 非同步

我們以常見的Web服務來舉例說明這一問題。

一般來說Web Server接收到使用者請求後會有一些典型的處理邏輯,最常見的就是資料庫查詢(當然,你也可以把這裡的資料庫查詢換成其它I/O操作,比如磁碟讀取、網路通訊等),在這裡我們假定處理一次使用者請求需要經過步驟A、B、C然後讀取資料庫,資料庫讀取完成後需要經過步驟D、E、F,就像這樣:

# 處理一次使用者請求需要經過的步驟:

A;
B;
C;
資料庫讀取;
D;
E;
F

其中步驟A、B、C和D、E、F不需要任何I/O,也就是說這六個步驟不需要讀取檔案、網路通訊等,涉及到I/O操作的只有資料庫查詢這一步。

一般來說這樣的Web Server有兩個典型的執行緒:主執行緒和資料庫處理執行緒,注意,這討論的只是典型的場景,具體業務實際上可會有差別,但這並不影響我們用兩個執行緒來說明問題。

首先我們來看下最簡單的實現方式,也就是同步。

這種方式最為自然也最為容易理解:

// 主執行緒
main_thread() {
    A;
    B;
    C;
    傳送資料庫查詢請求;
    D;
    E;
    F;
}

// 資料庫執行緒
DataBase_thread() {
    while(1) {
        資料庫讀取;
    }
}

這就是最為典型的同步方法,主執行緒在發出資料庫查詢請求後就會被阻塞而暫停執行,直到資料庫查詢完畢後面的D、E、F才可以繼續執行,就像這樣:

1600994106960
1600994106960

從圖中我們可以看到,主執行緒中會有“空隙”,這個空隙就是主執行緒的“休閒時光”,主執行緒在這段休閒時光中需要等待資料庫查詢完成才能繼續後續處理流程。

在這裡主執行緒就好比監工的老闆,資料庫執行緒就好比苦逼搬磚的程式設計師,在搬完磚前老闆什麼都不做只是緊緊的盯著你,等你搬完磚後才去忙其它事情。

顯然,高效的程式設計師是不能容忍主執行緒偷懶的。

是時候祭出大殺器了,這是什麼大殺器呢,關於這個問題的答案你可以參考這裡

總結

在這篇文章中我們從各種場景分析了同步與非同步這兩個概念,但是不管在什麼場景下,同步往往意味著雙方要相互等待、相互依賴,而非同步意味著雙方相互獨立、各行其是。希望本篇能對大家理解這兩個重要的概念有所幫助。

相關文章