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


在這篇文章中我們來討論一下到底什么是同步,什么是異步,以及在編程中這兩個概念到底意味着什么,這些是進一步掌握高性能、高並發技術的基礎,因此非常關鍵。

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

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

從圖中我們可以看到,主線程中會有“空隙”,這個空隙就是主線程的“休閑時光”,主線程在這段休閑時光中需要等待數據庫查詢完成才能繼續后續處理流程。

在這里主線程就好比監工的老板,數據庫線程就好比苦逼搬磚的程序員,在搬完磚前老板什么都不做只是緊緊的盯着你,等你搬完磚后才去忙其它事情。

顯然,高效的程序員是不能容忍主線程偷懶的。

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

總結

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM