哦哇資訊網

我是怎麼讀程式碼的

由 Thoughtworks 發表于 美食2023-01-17

作為一名程式設計師,總有一些時候,會對自己所做的重複性的工作感到厭倦,也會羨慕明星專案做得熱火朝天 Star 數蹭蹭上漲。而讀程式碼,則是緩解焦慮的良方。

每當讀懂軟體的精彩設計,讚歎優美整潔的程式碼,甚至發現藏在註釋中的彩蛋時,都好像在不同的時空與作者產生了交叉,暢快地聊了會兒天。

讀程式碼很有趣,但要讀通讀懂也很費功夫。本文是我在日常讀程式碼中積累的一點心得,分享出來,希望能與大家產生共鳴。

1。 尋找一位好老師

優秀的專案就像一位好老師,我們可以從它身上全方位地學到各種領域知識。

不過在開始讀程式碼之前, 最大的問題就是:怎麼樣才能找到合適的程式碼專案?

Star 數高的專案更優秀嗎?從某些角度講的確是的,但是在 gitstar-ranking 上, 4k 個 Star 的 Repo 只能排到第 5000 名,而至少有 50k 個 Star 才有可能排進前一百。光看 Star 數,這專案太多了根本選不出來啊。

在我看來,抓住如下幾個方向,一般就能篩選到合適的專案了:

興趣使然

首先就是一定要選自己感興趣領域的專案。

不少程式碼片段都是比較枯燥而難以閱讀的(比如“飛一般”的位操作,為提升效能而莫名其妙的語句,或是包含了大量隱含知識等等),只有自己感興趣,才會有讀下去的意願和動力,才能在其中發現樂趣。

有時候我們開始讀程式碼的契機就單純只是在工作當中用到了,想要進一步瞭解其原理和設計,這是一個不錯的起點。想必很多同學第一次看原始碼都是因為一層層追到了 Spring 的 Class 裡面去了吧。

有的時候可能就是覺得某項技術很神奇,像魔法一樣,越是猜不透,就越想了解它是怎麼“施法”的。

總之一旦有了興趣,就會很想進一步去了解它。不過,如果讀到一半又失去了興趣,也請大膽放棄它。失去了這一片草叢,還有整個樹林等著我們去探索。

經典且被大量使用

經典的、擁有大量使用者的專案,經歷了時間的考驗,不斷地迭代,通常在設計上都有很多出眾之處。

經典專案的維護者一般都是非常資深的工程師,並且也都會有大公司贊助,確保了程式碼的高質量。這類專案在閱讀的過程中能學到很多知識,包括架構抽象、效能最佳化、工程化等等。

比較常見的典型的專案有如:Go、Kubernetes、MySQL 等等。

合適的規模

程式碼量太過龐大的專案,有時雖然很出名,但難免令人生畏。實際上可以找到很多行數不多,但依然精彩的程式碼庫。

首先就是各種語言的標準庫,比如 Java 的 Stream、Lock 的實現等。另外也有不少開源的小而美專案,比如 redis、leveldb 等。甚至,許多經典大學課程裡面的 Lab 也隱藏著優秀的程式碼,比如 xv6。

總之,這一類程式碼可能幾天或幾周就能大致看完主幹,特別適合學習設計思想。

2。 先看文件

選定了專案,我們就差不多能對它有一些淺嘗輒止的瞭解了。

這時候,先不要直接 Clone 程式碼。程式碼完整地包含了所有知識,但也將細節毫無保留地暴露出來,直接進到程式碼裡面很容易迷失方向。

對於事物的理解,從全域性到部分,從抽象到細節才是一個比較容易讓人接受的過程。

成熟的專案通常會有比較詳盡的文件,文件一般分為兩類:給使用者看的使用文件,和給貢獻者看的開發文件。

瞭解概覽

透過閱讀使用文件我們能快速地瞭解到專案創立的目的、解決了哪些問題,以及從使用者的視角看該軟體是什麼樣子的。除了看 overview,我也會大致關注配置,透過必填配置可以進一步瞭解軟體的依賴和外部特性。

以 TiDB 為例,它的使用文件截圖如下:

從左側邊欄能瞭解到使用文件的結構包括了簡介、部署、配置、參考等部分。這些部分都是使用者最關心的內容。

架構和模組

優秀的開發文件一定會包括整個軟體的架構模型,和關鍵模組的設計。

透過閱讀架構圖和高層設計,軟體的原理以及解決問題的思路一目瞭然。對於有一定經驗的讀者甚至可能看到架構設計後就已經大概知道軟體的工作流程了。

上圖是 TiDB 開發文件截圖,我們發現它不僅包含了架構設計,還事無鉅細的告訴讀者如何啟動程式碼、怎樣貢獻、詳細的設計流程等等。

除了架構設計,比較完善的開發文件也會包含關鍵模組的資訊。關鍵模組可能會涉及核心邏輯的設計和資料結構,以及邊界處的契約和互動方式等。

對於 Go 語言這種持續演進的開源程式語言,甚至專門建立了一個 Proposal 倉庫來追蹤各種提案的設計、討論、程式碼以及釋出的情況,比如等了 10 年的泛型提案:

搞明白了架構模型和關鍵模組,真正開啟原始碼時就能將包、檔名、介面等包含的知識與整體結構相互對映,在腦中形成一張完整的圖。

其他的前置知識

有時候文件的作者還會加上不少前置知識,比如基於什麼樣的演算法,受到了哪些知識的啟發甚至是實現了哪篇論文的思想等等。

這些前置知識,對我們的理解會大有幫助。我們可以透過學習這些知識來進一步瞭解軟體的細節設計。

上圖是 etcd 的 github 頁面,在顯眼位置標明瞭它採用 Raft 共識演算法,並連結到 Raft 演算法的主頁,如果我們沒了解過 Raft,直接去讀 etcd 的程式碼,很可能就對裡面的選舉、日誌複製等概念一知半解,這就好像在看沒有字幕的外文電影,精彩程度大打折扣。

3。 再讀程式碼

看完了文件,就可以開始看程式碼了。為了防止在程式碼中迷失方向,我們可以遵循幾條原則來閱讀:

從入口開始

雖說透過架構模型以及包和檔案劃分的關係,我們能大致確定哪些程式碼是核心程式碼,但從入口處開始看會更符合大腦的思考方式。

因為入口程式碼的工作一般是先對各種模組進行初始化,然後調起主執行緒或者啟動主服務,這種明確順序的簡單工作讓我們不會一開始就遇到困難,循序漸進的過程更容易讓大腦產生獎勵。

如圖所示是 kubelet 啟動入口簡化後的主線邏輯,非常清晰。以此為起點沉下去,就可以分三路去細看配置詳情、建立 kubelet 的詳情,以及啟動的詳情。

抓住主線,從抽象到實現

主線就是從輸入是怎麼樣一步步產生輸出的。在這一過程中,會涉及到多個模組,每一個模組又有自己的輸入和輸出。

當我們順著函式呼叫、資料傳輸方向一步步向下時,隨著抽象層次的不斷降低,涉及到越來越多的細節,這個時候應該及時折返,不要一路看到底,很容易迷失在裡面。

良好的設計會有合理的抽象,根據不同的開發語言,我們可以透過檢視包、介面、特性、公有方法列表、標頭檔案等等來快速獲取抽象資訊,逐步地拼接出程式主線。搞清楚了主線,再逐步將抽象展開,閱讀具體實現程式碼。

仍以 kubelet 為例,kubelet 作為負責整個節點運轉的核心,工作多且雜。

我是怎麼讀程式碼的

但看它的程式碼分包結構,非常清楚地將不同功能點劃分到不同目錄下,結合初始化邏輯,再進一步深入到每個功能目錄內,又可以發現 kubelet 的模組設計遵循的是多個 manager 圍繞著一個核心共同協作的模型。好的抽象,就像一顆洋蔥一樣,層層分明。

一邊閱讀一邊記錄

初識一個專案,對結構和流程把握的不會太清楚,因此一邊讀一邊寫寫畫畫是很重要的。

有的時候跳轉次數比較多,前面看過的東西后面就忘記了,所以對關鍵路徑,記錄具體的函式名、模組名,能幫助我們快速回溯到入口。

也有的時候遇到了需要拓展的知識盲區,為了不打斷主線思路,可以先記錄下來,找其他時間再學習。

另外,遇到不直觀的、難以形成概念的程式碼表達,翻來覆去的看也看不懂,這個時候就需要畫個圖來幫助理解了。

一個典型的例子就是在學習 B+Tree 的分裂、合併、上移下移的時候,全看程式碼特別不直觀,想要理解這類內容畫圖定有奇效:

我是怎麼讀程式碼的

必要時藉助 debug

有一些程式碼為了正確性、效能等考慮,其表述可能會讓人百思不得其解。人類的思維方式是偏向順序的,用軟體開發做類比就是,我們更容易理解 Happy Path,而忽視分支細節。

當橫豎想不通某段程式碼為什麼要這麼寫的時候,實際執行一遍,加斷點 Debug 一下可能就會發現真實的原因了。

一個有趣的例子是:在環形佇列中,判斷佇列是否為空需要看頭指標和尾指標是不是已經重合,下圖的程式碼來自一個無鎖環形佇列的判空實現。

我是怎麼讀程式碼的

道理上講,環形佇列入隊 tail++,出隊 head++,先有入隊,才會有出隊,所以 tail 一定比 head 大。那為什麼上面程式碼裡,除了判斷 tail - head == 0 以外,還一定要加上當 tail < head 時也認為空呢,這根本不可能發生啊?

實際的原因是,由於該環形佇列是無鎖的, tail 和 head 之間不保證任何同步,那麼就可能由於排程因素,導致不同執行緒讀到不同時刻的值,結果 tail < head 就真的產生了。

想要搞清楚這種場景,最好的辦法就是真正執行幾百萬次測試,透過條件斷點讓程式碼在發生 tail < head 時停住,再觀察記憶體中的值來解釋。

4。 寫篇文章講講整個設計

程式碼看個七七八八,差不多就對設計和實現都有一定的認識了。這時候心裡多少會有點衝動想要把獲得的知識講出來。那麼最好就是寫篇文章,寫文章可以對知識進行梳理,在寫的過程中也會不斷加深印象。隨著文章的撰寫,作者的設計意圖亦會越來越清晰,對軟體的理解也會越來越深刻。

整理大綱

寫文章,目錄最關鍵。一篇文章是不是有邏輯性,結構是不是清晰,全都在大綱的設計上。

既然我們的內容是講軟體的設計與實現,那麼文章的大綱就可以按 Why - What - How 來展開:先告訴讀者為什麼要設計該軟體,它解決了哪些問題。之後講述軟體的架構模型、關鍵模組以及主線流程。最後詳細地講解具體實現。

在寫文章之初,我們的知識還不夠深入和整體,可以先寫 what 和 how 的部分,加深理解之後,就能明白設計的 why 了。

以我寫的一篇介紹 Go 語言 Runtime 計算模型設計的文章目錄為例:

我是怎麼讀程式碼的

可見大致就是按照 “設計意圖和目標 -> 核心概念解釋 -> 具體實現 -> 總結拓展” 這樣的順序來設計的整體文章結構。這種結構清晰、簡單明瞭的目錄結構很適合用在技術文章上。

描述設計原理,透過畫圖幫助分析設計意圖

在介紹原理和實現的時候,相比於貼程式碼,更好的方式是透過畫圖來表達。程式碼的確能體現全部的設計細節,但程式碼更重要的任務是作為知識和硬體指令之間的橋樑。相反,如果我們用圖表的形式表達設計意圖,就會對人類更友好,更容易閱讀、理解和學習。畫圖本身也是一種加深理解,去粗取精的過程。

下圖是我讀了 leveldb 之後畫的 leveldb 儲存架構圖:

我是怎麼讀程式碼的

作為儲存引擎,LSM Tree 的實現是 leveldb 的核心,leveldb 本身原始碼已經很清晰、簡潔,但如果透過上面這樣一張圖來講述其 LSM Tree 的具體設計,一定會比貼程式碼要易懂得多。

想一想,為什麼要這麼設計,好處在哪裡?

當我們能用圖表和文字來表達出軟體的完整設計後,我們對程式碼的理解已經比較透徹,甚至,讓我們自己來照著寫一個新的也不是不可能了。

這個時候,就應該進一步的思考,如果是我自己來解決問題,我會怎麼做?我能比原作者做得更好嗎(通常不能)?

在思考為什麼這麼設計的時候,如果相關領域知識不充足,就會驅使我們去查詢很多參考資料,瞭解和借鑑別人看問題的角度。找資料的過程總有驚喜,如果能讀到一些非常深入淺出的文章,而後就會懷著敬佩之情,收藏、關注作者的部落格,想想如果不是因為讀了某段程式碼,還真無緣遇到這些精彩的文章和優秀的作者。

我在讀 Go 語言記憶體管理程式碼的時候,一開始搞懂了 tcmalloc 的原理和實現,但對其所謂執行緒快取、無鎖分配等等賣點理解不深刻。直到回過頭去讀了 CSAPP 動態記憶體分配的章節,又結合 ptmalloc、jemalloc 的設計,相互對比理解,這才更清晰的認識了 tcmalloc 的設計決策。

經過這一階段的思考並結合其他人的理解之後,我們就能清楚地意識到,軟體所面臨問題的限制條件是什麼,作者這樣設計的好處有哪些。把這部分寫完,新增到文章的最開始,就比較完美了。

這一節講了一點關於寫文章的內容,對於技術寫作,推薦 Thoughtworks 洞見團隊出品的 《技術寫作手冊》。

5。 講個 Session,收穫 Extra Bonus

如果還有精力和興致,那不如把文章的內容提取出來做個 Session 講給大家,額外的付出能收穫額外的獎賞。

有過做講師經歷的同學肯定會知道,給別人講東西,收穫最大的不是聽眾,而是講師本人。想要輸出一小時的 Session,所花費的準備時間可能要十個小時。我們需要花費數倍於講解的時間來完善素材,理清思路,準備問題,甚至還包括思考可能會涉及到的拓展內容。做這些工作在提升我們 session 質量的同時,無形中也不斷地強化了我們對相關知識的認知。

梳理要點,邏輯自洽

一個 Session 成功的基礎在於能不能邏輯自洽。而邏輯自洽的前提就是關鍵要點必須清晰,並且前後可以呼應。

上一節提到的文章,正好就是 Session 材料的源泉,因此我會反覆遍歷整篇文章,期望從中抽取所需的內容。這個過程往往伴隨著不斷地發現文章的內容缺失、邏輯不通之處,這時文章就得到了進一步的改善。所以經常發現,當整個 Slide 完整的順下來後,不僅成就感爆棚,文章也豐滿了,理解還更深刻了。

去粗取精,鍛鍊表達

相比寫文章,講 Session 要我們進一步的去除細節,只保留最核心的思想,這本身是對抽象能力的一種鍛鍊。

另外,自己瞭解清楚,和能給別人講清楚是完全不同的兩種概念。如何能把核心知識講給聽眾,並且能讓聽眾更容易的聽懂,需要仔細地思考語言的表達。每一次成功的 Session 都是對自己表達能力的一次提升。

表達上最常見的問題就是照著文字念。我個人喜歡透過減少 Slide 中文字的數量,來倒逼自己提升表達的邏輯性與連貫性。可以嘗試思考,如果內容只是一張圖,那麼要怎麼講清楚這張圖,用這種辦法訓練表達能力。

揣摩聽眾感興趣的方向

考慮聽眾的感受也很重要,如果講的內容大家不感興趣,不愛聽,或是晦澀難懂,跟不上節奏,就容易導致整個 Session 反響寥寥,大家會覺得來聽你的 Session 浪費了自己的時間。

所以不僅要能講清楚,還要揣摩聽眾感興趣的方向。合理的設定內容,去除枯燥乏味而非關鍵性的東西,並且調整講解的順序,把易懂的、精彩的部分穿插放置,這樣就可以不斷地激發聽眾的興趣。

最後,線下組織的效果要比線上影片講解好得多。線上下聽眾的注意力更集中,互動效果好,演講者也更容易透過聽眾的表情、神態來判斷是否需要調整內容和速度。如果因為條件限制一定要做影片 Session,那麼可能需要經常停頓下來問些問題,或是主動的尋求反饋。

6。 結語

本文是我日常讀程式碼的一點經驗,總結下來,就是要

仔細地選擇學習的專案;

先透過文件瞭解全景,再逐步深入程式碼;

找對抽象和邊界,能幫助我們建立思考模型;

寫篇文章講述程式碼的設計,是深入理解程式碼的好辦法;

自己學會了還不夠,能清楚地講給別人才是真正的掌握。

最後祝願所有讀者都能從程式碼中獲得最大的樂趣。

文/Thoughtworks 張旭海

更多精彩洞見,請關注公眾號Thoughtworks洞見

TAG: 程式碼設計文件session知識