編程世界的熵增原理

歌者沒有太多的抱怨,生存須要投入更多的思想和精力。
宇宙的熵在升高,有序度在下降,像平衡鵬那一望無際的黑翅膀,向存在的一切壓下來,壓下來。但是低熵體不同,低熵體的熵還在下降,有序度還在上升,像漆黑海面上升起的磷火,這就是意義,最高層的意義,比樂趣的意義層次要高。要維持這種意義,低熵體就必須存在和延續。
html

對科幻有一點了解的朋友也許已經猜到,這段描寫出自《三體》。這想必是整部《三體》中最燒腦的一段文字了。java

歌者反覆提到的「低熵體」,究竟是一個怎樣的存在呢?要理解它,咱們首先要來說講「熵」這個概念。程序員

聽說,在不少物理學家的眼中,科學史上出現的最重要的物理規律,既不是牛頓三大定律,也不是相對論或者宇宙大爆炸理論,而是熱力學第二定律。它在物理規律中具備至高無上的地位,由於它從根本上支配了咱們這個宇宙演化的方向。這個定律指出:任何孤立系統,只能沿着熵增長的方向演化。數據庫

什麼是熵?通俗來說,能夠理解爲物體或系統的無序狀態,或者混亂程度(亂度)。在沒有外力干涉的狀況下,隨着時間的推移,一個系統的亂度將會愈來愈大。將冰塊投入溫水中,它終將融化,並與水交融爲一體,由於溫水的無序程度要高於冰塊;一副撲克牌,即便按照花色和大小排列整齊,但通過屢次隨機的洗牌以後,它終將變得混亂無序;一間乾淨整潔的房間,若是長期沒有人收拾的話,它將會變得髒亂不堪。編程

而生命體,尤爲是智慧生命體(好比人類),倒是典型的「低熵體」,可以維持自身和周圍環境長期處於低熵的狀態。能夠想象,若是一所房子可以長期保持乾淨整潔,多半是由於它有一位熱愛整潔且勤於家務的女主人。後端

縱觀整我的類的發展史,人們將荒野開墾成農田,將河流疏導成生命的水源,結束散居生活從而彙集成村落。同時,人類又花費了數千年的時間,創建起輝煌的城市文明。城市道路和建築樓羣排列有致,軌道交通也井井有理;城市的地下管線錯綜複雜,爲每家每戶輸送水電能源;清潔工人天天清掃垃圾,並將它們分門別類,運往恰當的處理地點……服務器

全部的這一切,得以讓咱們這個世界遠離無序狀態,將熵值維持在一個很低的水平。微信

可是,一旦離開人類這個「低熵體」的延續和運轉,這一切整齊有序都將不復存在。甚至是當人類的單個個體死亡以後,它的有機體也再不能維持自身。它終將隨着時間腐爛,最終化爲泥土。網絡


記得在開發微愛App的過程當中,咱們曾經實現過這樣一個主題皮膚的功能:數據結構

按照上面的截圖所示,用戶能夠將軟件的顯示風格設置成多種主題皮膚中的一個(上面截圖中顯示了8個可選的主題)。固然,用戶同一時刻只能選中一個主題。

咱們的一位工程師按照這樣的思路對存儲結構進行了設計:每一個主題用一個對象來表示,這個對象裏存儲了該主題的相關描述,以及該主題是否被用戶選中(做爲當前主題)。這些對象的數據最初都是從服務器得到的,都須要在本地進行持久化存儲。對象的數據結構定義以下(僞碼):

/** * 表示主題皮膚的類定義。 */
public class Theme {
    //該主題的ID
    public int themeId;
    //該主題的名稱
    public String name;
    //該主題的圖片地址
    public String picture;

    //其它描述字段
    ......

    //該主題是否被選中
    public boolean selected;
}

/** * 全局配置:保存的各個主題配置數據。 * 從持久化存儲中得到。 */
Theme[] themes = getFromLocalStore();複製代碼

上面截圖界面中的主題選中狀態的顯示邏輯以下(僞碼):

//輸入參數:
//界面中顯示各個主題的View層控件
View[] themeViews;

......

for (int i = 0; i < themeViews.length; i++) {
    if (themes[i].selected) {
        //將第i個主題顯示爲選中狀態
        displaySelected(themeViews[i]);
    }
    else {
        //將第i個主題顯示爲未選中狀態
        displayNotSelected(themeViews[i]);
    }
}複製代碼

而用戶從新設置主題的時候,選中邏輯以下(僞碼):

//輸入參數:
//界面中顯示各個主題的View層控件
View[] themeViews;
//當前用戶要選擇的新主題的下標
int toSelect;

......

//找到舊的選中主題
int oldSelected = -1;
for (int i = 0; i < themes.length; i++) {
    if (themes[i].selected) {
        oldSelected = i; //找到了
        break;
    }
}

if (toSelect != oldSelected) {
    //修改當前選中的主題數據
    themes[toSelect].selected = true;
    //將當前選中的主題顯示爲選中狀態
    displaySelected(themeViews[toSelect]);

    if (oldSelected != -1) {
        //修改舊的選中主題的數據
        themes[oldSelected].selected = false;
        //將舊的選中主題顯示爲非選中狀態
        displayNotSelected(themeViews[oldSelected]);
    }

    //最後,將修改後的主題數據持久化下來
    saveToLocalStore(themes);
}複製代碼

這幾段代碼看起來是沒有什麼邏輯問題的。可是,在用戶使用了一段時間以後,有用戶給咱們發來了相似以下的截圖:

居然同時選中了兩個主題!而咱們本身無論怎樣測試都重現不了這樣的問題,檢查代碼也沒發現哪裏有問題。

這究竟是怎麼回事呢?

通過仔細思考,咱們終於發現,按照上面這個實現,系統具備的「熵」比它的理論值要稍微高了一點。所以,它纔有機會出現這種亂度較高的狀態(兩個同時選中)。

什麼?一個軟件系統也有熵嗎?各位莫急,且聽我慢慢道來。

熱力學第二定律,咱們通俗地稱它爲熵增原理,乃是宇宙中至高無上的廣泛規律,在編程世界固然也不例外。

爲了從程序員的角度來解釋熵增原理的本質,咱們仔細分析一下前面提到過的撲克牌洗牌的例子。我第一次看到這個例子,是在一本叫作《悖論:破解科學史上最複雜的9大謎團》的書上看到的。再也沒有例子可以如此通俗地表現熵增原理了。

從花色和大小整齊排列的一個初始狀態開始隨機洗牌,撲克牌將會變得混亂無序;而反過來則不太可能。想象一下,若是咱們拿着一副完全洗過的牌,繼續洗牌,而後忽然出現了花色和大小按有序排列的狀況。咱們必定會認爲,這是在變魔術!

系統的演變爲何會體現出這種明確的方向性呢?本質上是系統狀態數的區別

花色和大小有序排列,只有一種狀況,因此狀態數爲1;而混亂無序的排列方式的數量,是一個很是很是大的值。稍微應用一點組合數學的知識,咱們就能算出來,全部混亂無序的排列方式,總共有(54!-1)種,其中(54!)表示54的階乘。混亂的狀態數多到數不勝數,所以隨機洗牌過程老是使牌序壓倒性地往混亂無序的方向發展。

而混亂無序的反面——整齊有序,則本質上意味着對於系統可取狀態數的限制。對於全部54張牌,咱們限制只能取一種特定的排列,就意味着整齊。一樣,在整潔的房間裏,一隻襪子不會出如今鍋裏,或者其它任意地方,也是一種對於可取狀態的限制。

咱們編程的過程,就是根據每個條件分支,逐漸細化和限制系統的混亂狀態,從而最終達到有序的一個過程。咱們構建出來的系統,對於可取狀態數的限制越強,系統的熵就越低,它可能達到的狀態數就越少,就越不可能進入混亂的狀態(也是咱們不須要的狀態)。

回到剛纔主題皮膚的那個例子,假設總共有8個主題,按前面的實現,每一個主題都有「選中」和「未選中」兩個狀態。那麼,系統總的可取狀態數一共有「2的8次方」個,其中有8個狀態是咱們所但願的(也就是有序的狀態,分別對應8個主題分別被選中的狀況),剩餘的(2的8次方-8)個狀態,都屬於混亂狀態(錯誤狀態)。前面出現的兩個主題被同時選中的狀況,就屬於這其中的一種混亂狀態。

在前面的具體實現中,程序邏輯已經在盡力將系統狀態限制在8個有序狀態上,但實際運行的時候仍是進入了某個混亂狀態,這是爲何呢?

由於一個具體的工程實現,是要面對很是複雜的工程細節的,幾乎沒有一個邏輯是可以被完美實現的。也許在某個微小的實現細節上出現了意想不到的狀況,也許是持久化的時候沒有正確地運用事務處理,也可能有來自系統外的干擾。

可是,對於這個例子來講,咱們其實能夠在限制系統狀態方面作得更好。有些同窗可能已經看出來了,表示主題「選中」和「未選中」的狀態,其實不該該保存在每一個主題對象中(Theme類),而應該全局保存一個當前選中的主題ID,這樣,全部可能的選中狀態就只有8個了。

修改以後的數據結構以下(僞碼):

/** * 表示主題皮膚的類定義。 */
public class Theme {
    //該主題的ID
    public int themeId;
    //該主題的名稱
    public String name;
    //該主題的圖片地址
    public String picture;

    //其它描述字段
    ......
}

/** * 各個主題數據。 */
Theme[] themes = ...;

/** * 全局配置:當前選中的主題的ID。 * 初始值是默認主題的ID。 */
 int currentThemeId = getFromLocalStore(DEFAULT_CLASSIC_THEME_ID);複製代碼

顯示邏輯修改後以下(僞碼):

//輸入參數:
//界面中顯示各個主題的View層控件
View[] themeViews;

......

for (int i = 0; i < themeViews.length; i++) {
    if (themes[i].themeId == currentThemeId) {
        //將第i個主題顯示爲選中狀態
        displaySelected(themeViews[i]);
    }
    else {
        //將第i個主題顯示爲未選中狀態
        displayNotSelected(themeViews[i]);
    }
}複製代碼

用戶從新設置主題的時候,修改後的選中邏輯以下(僞碼):

//輸入參數:
//界面中顯示各個主題的View層控件
View[] themeViews;
//當前用戶要選擇的新主題的下標
int toSelect;

......

//找到舊的選中主題
int oldSelected = -1;
for (int i = 0; i < themes.length; i++) {
    if (themes[i].themeId == currentThemeId) {
        oldSelected = i; //找到了
        break;
    }
}

if (toSelect != oldSelected) {
    //修改當前選中主題的全局配置
    currentThemeId = themes[toSelect].themeId;
    //將當前選中的主題顯示爲選中狀態
    displaySelected(themeViews[toSelect]);

    if (oldSelected != -1) {
        //將舊的選中主題顯示爲非選中狀態
        displayNotSelected(themeViews[oldSelected]);
    }

    //最後,將修改後的主題數據持久化下來
    saveToLocalStore(currentThemeId);    
}複製代碼

這個例子雖然簡單,但卻很好地體現出了軟件系統的熵值的概念。

咱們編程的過程,實際上就是不斷地向系統輸入規則的過程。經過這些規則,咱們將系統的運行狀態限制在那些咱們認爲正確的狀態上(即有序狀態)。所以,避免系統出現那些不合法的、額外的狀態(即混亂狀態),是咱們應該竭力去作的,哪怕那些狀態初看起來是「無害」的。


第二個例子

若干年前,當咱們在某開放平臺上開發Web應用的時候,發生過這樣一件事。

咱們當時的某位後端工程師,打算在新用戶第一次訪問咱們的應用的時候,爲用戶建立一份初始數據(UserData結構)。同時,在當前訪問請求中還要向用戶展現這份用戶數據。這樣的話,若是是老用戶來訪問,那麼展現的就是該用戶最新積累的數據;相反,若是來訪的是新用戶的話,那麼展現的就是該用戶剛剛初始化的這份數據。

所以,這位工程師設計並實現了以下接口:

UserData createOrGet(long userId);複製代碼

在這個接口的實現中,程序先去數據庫查詢UserData,若是能查到,說明是老用戶了,直接返回該UserData;不然,說明是新用戶,則爲其初始化一份UserData,並存入數據庫中,而後返回新建立的這份UserData。

若是這裏的UserData確實是一份很基本的用戶數據,且上述接口的實現編碼得當的話,這裏的作法是沒有什麼大問題的。對於通常的應用來講,用戶基本數據一般在註冊時建立,在登陸時查詢。而對於開放平臺的內嵌Web應用來講,第一個訪問請求每每同時帶有註冊和登陸的性質,所以將建立和查詢合併在一塊兒是合理的。

可是不久,應用內就出現了另一些查詢UserData的需求。既然原來已經有一個現成的createOrGet接口了,並且它確實能返回一個UserData對象,因此這位工程師出於「代碼複用」的考慮,在這些須要查詢UserData的地方調用了createOrGet接口。

通過本文前面的討論,咱們不難看出這樣作的問題:這種作法無心間讓系統的熵增長了。在本該是查詢的邏輯分支上,程序不得不處理跟建立有關的額外邏輯和狀態,而這些多餘的狀態增長了系統進入混亂的機率。

第三個例子

在這一部分,咱們討論一個稍微複雜一點的例子,它跟消息發送隊列有關。

假設咱們要開發一個IM軟件,就跟微信相似。那麼,它發送消息(Message)的時候,不該該只是提交一次網絡請求這麼簡單。

  • 首先,先後屢次發送消息的各個請求須要排隊;
  • 其次,因爲網絡環境很差而形成請求失敗時,應該在必定程度上可以重試請求。
  • 第三,請求隊列自己須要持久化,這樣即便軟件重啓,未發送完的消息也可以繼續發送。

所以,咱們須要爲發送消息建立一個有排隊、重試和本地持久化功能的發送隊列。

關於持久化,其實除了發送隊列自己須要本地持久化,用戶輸入和接收到的聊天消息,也須要本地持久化。當消息發送成功後,或者當消息嘗試屢次最終仍是失敗以後,該消息在發送隊列的持久化存儲裏刪除,可是仍然保存在聊天消息的持久化存儲裏。

通過以上分析,咱們的發送消息的接口(send),實現以下(僞碼):

public void send(Message message) {
    //插入到聊天消息的持久化存儲裏
    appendToMessageLocalStore(message);
    //插入到發送隊列的持久化存儲裏
    //注:和前一步的持久化操做應該放到同一個DB事務中操做,
    //這裏爲了演示方便,省去事務代碼
    appendToMessageSendQueueStore(message);

    //在內存中排隊或者當即發送請求(帶重試)
    queueingOrRequesting(message);
}複製代碼

其中,表示消息的類Message,以下定義(僞碼):

/** * 表示一個聊天消息的類定義。 */
public class Message {
    //該消息的ID
    public long messageId;
    //該消息的類型
    public int type;

    //其它描述字段
    ......
}複製代碼

如前所述,當網絡環境很差而形成請求失敗時,發送隊列會嘗試重試請求,但若是連續失敗不少次,最終發送隊列也只能宣告發送失敗。這時候,在用戶聊天界面上一般會標記該消息(好比在消息旁邊標記一個紅色的歎號)。用戶能夠等待網絡好轉以後,再次點擊該消息來從新發送它。

這裏的從新發送,能夠仍然調用前面的send接口。可是,因爲這個時候消息已經在持久化存儲中存在了,因此不該該再調用appendToMessageLocalStore了。固然,保持send接口不變,咱們能夠經過一個查詢操做來區分是第一次發送仍是重發。

修改後的send接口的實現以下(僞碼):

public void send(Message message) {
    Message oldMessage = queryFromMessageLocalStore(message.messageId);
    if (oldMessage == null) {
        //沒有查到有這個消息,說明是首次發送
        //插入到聊天消息的持久化存儲裏
        appendToMessageLocalStore(message);
    }
    else {
        //查到有這個消息,說明是重發
        //只是修改一下聊天消息的狀態就能夠了
        //從失敗狀態修改爲正在發送狀態
        modifyMessageStatusInLocalStore(message.messageId, STATUS_SENDING);
    }
    //插入到發送隊列的持久化存儲裏
    //注:和前面兩步的查詢操做以及插入和修改操做
    //應該放到同一個DB事務中操做,
    //這裏爲了演示方便,省去事務代碼
    appendToMessageSendQueueStore(message);

    //在內存中排隊或者當即發送請求(帶重試)
    queueingOrRequesting(message);
}複製代碼

可是,若是按照本文前面分析的編程的熵增原理來看待的話,這裏對於send的修改使得系統的熵增長了。原本首次發送和重發這兩種不一樣的狀況,在調用send以前是很清楚的,但進入send以後咱們卻丟失了這個信息。所以,咱們須要在send的實現裏面再依賴一次查詢的結果來判斷這兩種狀況(狀態)。

一個程序運行的過程,本質上是根據每個條件分支,從邏輯樹的頂端,一層一層地向下,選擇出一條執行路徑,最終到達某個終端葉子節點的過程。程序每進入新的下一層,它對於當前系統狀態的理解就更清晰了一點,也就是它須要處理的狀態數就少了一點。最終到達葉子節點的時候,就意味着對於系統某個具體狀態的肯定,從而能夠執行對應的操做,把問題解決掉。

而上面對於send的修改,卻形成了程序運行過程當中須要處理的狀態數反而增長的狀況,也就是熵增長了。

若是想要避免這種熵增現象的出現,咱們能夠考慮新增一個重發接口(resend),代碼以下(僞碼):

public void resend(long messageId) {
    Message message = queryFromMessageLocalStore(messageId);
    if (message == null) {
        //不可能狀況,錯誤處理
        return;
    }

    //只是修改一下聊天消息的狀態就能夠了
    //從失敗狀態修改爲正在發送狀態
    modifyMessageStatusInLocalStore(message.messageId, STATUS_SENDING);
    //插入到發送隊列的持久化存儲裏
    //注:和前一步的持久化操做應該放到同一個DB事務中操做,
    //這裏爲了演示方便,省去事務代碼
    appendToMessageSendQueueStore(message);

    //在內存中排隊或者當即發送請求(帶重試)
    queueingOrRequesting(message);
}複製代碼

固然,有的同窗可能會反駁說,這樣新增一個接口的方式,看起來對接口的統一性有破壞。無論是首次發送,仍是重發,都是發送,若是調用同一個接口,會更簡潔。

沒錯,這裏存在一個取捨的問題。

選擇任何事情都是有代價的。如何選擇,取決於你對於邏輯清晰和接口統一,哪個更看重。

固然,我我的更喜歡邏輯清晰的方式。


在熵增原理的統治之下,系統的演變體現出了明確的方向性,它老是向着表明混亂無序的多數狀態的方向發展。

咱們的編程,以及一切有條理的生命活動,都是在同這一終極原理對抗。

更進一步理解,熵增原理所體現的系統演變的方向性,其實正是時間箭頭的方向性。

它代表時間不可逆轉,一切物品,都會隨着時間的推移而逐漸損壞、腐化、衰老,甚至逐漸喪失與周圍環境的界限。

它是時間之神手裏的鐵律。

代碼也和其它物品同樣,不可避免地隨着時間腐化。

惟一的解決方式,就是耗費咱們的智能,不停地維持下去。有如文明的延續。

除非——

有朝一日,

AI出現。

也許,到那時,咱們的世界才能維持低熵永遠運轉下去。

那時的低熵體,也許會像歌者同樣,輕聲吟唱起那首古老的歌謠:

我看到了個人愛戀
我飛到她的身邊
我捧出給她的禮物
那是一小塊凝固的時間
時間上有美麗的條紋
摸起來像淺海的泥同樣柔軟
……





(完)


後記

本文總共列舉了三個編程的實際例子。我之因此選擇它們做爲例子,並非由於它們是最好的例子,而是由於它們相對獨立,也相對容易描述清楚。實際上,在平常的編程工做中,那些跟本文主旨有關的、涉及系統狀態表達和維護的取捨、折中和決策,幾乎隨時都在進行,特別是在進行接口設計的時候。只是這其中產生的思考也許大多都是靈光一閃,轉瞬即逝。本文嘗試把這些看似微小的思想彙集成篇,但願能對看到本文的讀者們產生一絲幫助。

其它精選文章

相關文章
相關標籤/搜索