歌者没有太多的抱怨,生存须要投入更多的思想和精力。
宇宙的熵在升高,有序度在下降,像平衡鹏那一望无际的黑翅膀,向存在的一切压下来,压下来。但是低熵体不同,低熵体的熵还在下降,有序度还在上升,像漆黑海面上升起的磷火,这就是意义,最高层的意义,比乐趣的意义层次要高。要维持这种意义,低熵体就必须存在和延续。
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出现。
也许,到那时,咱们的世界才能维持低熵永远运转下去。
那时的低熵体,也许会像歌者同样,轻声吟唱起那首古老的歌谣:
我看到了个人爱恋
我飞到她的身边
我捧出给她的礼物
那是一小块凝固的时间
时间上有美丽的条纹
摸起来像浅海的泥同样柔软
……
(完)
后记:
本文总共列举了三个编程的实际例子。我之因此选择它们做为例子,并非由于它们是最好的例子,而是由于它们相对独立,也相对容易描述清楚。实际上,在平常的编程工做中,那些跟本文主旨有关的、涉及系统状态表达和维护的取舍、折中和决策,几乎随时都在进行,特别是在进行接口设计的时候。只是这其中产生的思考也许大多都是灵光一闪,转瞬即逝。本文尝试把这些看似微小的思想汇集成篇,但愿能对看到本文的读者们产生一丝帮助。
其它精选文章: