為什麼說非同步編程是反人類?


callback,promise,await,就越來越人性化了。如果你有一個好的非同步庫,你會發現寫非同步代碼比寫同步代碼在處理並發之類的問題上簡單多了,幾乎從來不用考慮加鎖的問題。比如說可以試試VLCP

hubo1016/vlcp


對於從來沒有接觸過編程的人來說,非同步其實比線性更貼近人類的直覺。現實世界的組織結構本身就是由大量的並行線/進程靠非同步所結合起來的。

非同步只是單純不符合剛開始學過一點編程,思維被限制成線性的初學者的直覺。


軟體工程講究內聚性,就是邏輯相關的代碼最好寫一起,不要把邏輯相關 而且分開時毫無重用性的代碼分開寫在不同的函數里。

非同步代碼就經常破壞這個規範:因為一個流程內的若干子流程的可執行條件不在流程內去控制(我發個Http請求並且收回包最後渲染網頁,然而如果走無阻塞IO模式,當前流程是沒法知道什麼時候收包完畢並且可以渲染網頁的),所以不得不把這些子流程分開在不同的函數里,而且還得用比較奇怪的方式在這些子流程之間傳遞局部變數和參數(因為局部變數無法被子流程直接訪問到了,得把他生命周期延長,並把他的地址傳遞到某個公共空間。。)。要說反人類的話,就這個地方比較讓人頭疼了吧,不信可以去閱讀下Nginx redis這些開源7層網路服務大做。

支持閉包的語言其實寫非同步都並不特別痛苦:因為可以用閉包把不在一個上下文內執行的各個子流程寫在一起,再用「變數捕獲」的方法來傳遞參數。這就把上面提到的2個難題比較好的處理了。

支持閉包但是不支持垃圾回收的語言寫非同步也會比較痛苦:因為需要開發者去處理變數捕獲中的引用變數的生命周期。比較好的方法是使用shared_ptr這樣的方法去做變數捕獲(注意閉包捕獲是單向的,函數不會持有自己閉包中新建的任何變數的引用,所以不會產生循環引用的問題),shared_ptr的值捕獲語義其實就是引用捕獲;或者直接使用目前VC和clang所支持的resumable function, 讓編譯器來幫你實現把可能會被閉包捕獲的局部變數分配在堆裡面,並且在最後一個持有這個局部變數的引用捕獲的閉包執行完畢之後析構這個局部變數(這2種方案其實沒有本質區別,甚至後者同樣需要考慮閉包和函數不在一個線程中執行而不得不對引用計數進行原子操作,只是後者顯然語法會方便很多啦。)。


從得到需求,到開發人員制定好方案,倒寫代碼,到代碼生成彙編或者最終 CPU 上執行,有很多個步驟。極端一點的情況,手寫彙編交給 CPU 執行,有人也能做到,但是複雜的程序顯然不能這麼搞。因為缺乏語法糖,也缺乏抽象,人腦沒那麼厲害,這些輔助的能力需要編程語言來提供。

非同步就是類似的問題,有的語言對非同步沒有好的抽象,代碼一複雜,寫起來就很費勁。用外行人的話說,就是「反人類」。我認為 Go 語言實現的 CSP 是一個好的抽象。


EDGE瀏覽器支持await了哦


因為很多人連過程式編程的思維都沒有。

通俗的講,就是不喜歡把程序拆分為函數。非同步編程會強迫你執行這個步驟。

而且萬一遇到大量代碼內容=函數標題這樣的短過程,又不是不讓你用閉包= =(但是閉包內容太多會造成閱讀困難,請謹慎)

此外那些習慣於拆函數,依然覺得反人類的人也可能是在機械的使用非同步編程,導致程序的結構被破壞了。這東西使用的時候是有技巧的。

上面有人提到的Promise(以及類似方案)是個很常用的解決非同步可讀性的方法,雖然看上去像是非同步過程同步化吧……

「同步化」只是非同步編程的一個分支而已,非同步可以同步化,也可以非同步,也就是選擇更多樣,所以才是更優的解決方案。


手寫cps怎麼可能不反人類


並不反人類,只是你沒找到好庫。你需要rx。


這題目真是驚悚,其實主要是非同步不符合一般rd的思維,傳統代碼段一句句下來就可以,執行到哪很容易知道,結果async是在多個callback之間jump,而且還不知道什麼時候jump過去,這代碼看起來就糾結,更不用說debug了,所以後來很多概念出來了,諸如協程等,就是用同步寫非同步,方便rd使用,當然這也是趨勢。在攜程出現之前,為了解決各個callback難以管理的問題,不同框架有不同實現,比如twisted的defer等等。


非同步編程,本質上和在CPU中斷上編程一樣,是一種非常低級和開發低效的編程模式。相應的,同步編程才是「高級模式」,使用同步API編程可以寫出好得多的客戶代碼

不妨考慮這種情況:任何稍微複雜的代碼,都需要大量嵌套調用函數(nested function call),就像以下的代碼

f(g(), h());

使用同步編程最大的好處是,g、h可以選擇調用外部IO來獲取中間數據,而這一切f都不用關心。如果要想用非同步編程做到這一點,必須把所有的函數的輸入和輸出都設計成Promise才能做到不用關心函數內部是否需要IO來獲得中間數據,否則嵌套調用不能在任意情況下成立

「把所有的函數的輸入和輸出都設計成Promise」,如果這不是最典型的反模式(anti-pattern),那什麼又是呢?


回答這種沒啥技術含量的問題是吃力不討好的事,無奈自身水平低下看到簡單的問題忍不住手癢,呵呵。

首先要對問題本身提兩個問題:1、非同步編程不是一個良性的定義;2、在1的基礎上,反人類一詞就無從談起。

但問題提出了,很多人也肯定是內心有類似的疑惑才會關注/討論它。那麼問題實質核心是什麼呢?其實對人而言困難的事無非就是複雜性和不可預知性,而在一般編程中的只有前者就是單純複雜性。

非同步本身並不是什麼反人類的事,現實世界就是一個非同步的世界,事物獨立發展同時又普遍關聯,在大多數情況下,不會因為一兩件非同步的事情就讓人覺得困難,雖然打斷大腦思維有的時候也是頗讓人頭疼的。在計算機世界裡把關聯事物同時運行的特性稱為並發性,這是自操作系統問世以來就被研究再研究的問題,因為操作系統要做的事情,就是設想把共享的資源管理起來,提供給上層一個透明一致的介面。所以這個問題難不難,早期最有心得體會的是做操作系統的那幫人;隨著桌面,網路的發展,以及越來越多的計算核心,對一般的應用也有了越來越多的並發需求。所以準確的說法叫並發編程,非同步只是並發編程在某些特定手段下的表現。而且,在跟物理世界打交道時,非同步表現的會更加頻繁,這也是現實世界反映。

那麼為什麼並發(非同步)編程對人而言究竟困難在什麼地方呢?如果程序只處理一個非同步的事件,那麼就不存在什麼困難,因為(往往在操作系統的支撐下)很容易表現為一個同步的過程,短短的幾十上百行的代碼,或者哪怕更長,只要是順序執行的,總是很容易一步步的分解開來,被一個平凡人的大腦所理解和接受。

但並發編程(目前)往往不是這樣的,你需要頻繁的對程序執行路徑進行預測,為了最大化並發的效率,而把不相關的邏輯組合在一起,通過大腦的人肉執行對可能發生的情況一一做出妥善的安排,這不僅可能導致邏輯更加複雜化,而且很容易出現超長的耦合代碼;就算是做出各種設計上的斟酌後,你能夠把代碼塊解剖的看似更小更乾淨,但實際上是把本來順序執行的部分分成了相隔很遠的可能被稱為函數/介面的一個個散碎的功能。儘管設計的再怎麼完美,人腦機器已經幾乎失去了在短短的連續時間內對相關邏輯進行模擬運行的能力,也就是證明正確性這件事情已經支離破碎了,很難在花費不大的情況下對其進行保證。歸根到底也就是上面說的複雜性。

回過頭來,我們考慮一下這個複雜性是否是問題本身帶來的?顯然,不能刨除這個因素,但也不是全部,甚至可能非常的次要。可能很多人一下子難以接受這個觀點,我要問一句,既然提需求的那個傢伙,(就是大夥鄙夷的,背後被稱作xx狗的),能把問題描述的清楚,為何我們卻覺得困難?儘管從細節而言,我們考慮的要多得多,但這個和理解問題並沒有多大關係,難道大家在不同複雜度世界中描述問題嗎?如果不是這樣,那麼問題就肯定出在在把現實中描述問題對應計算機世界的這個環節中,畢竟對計算機世界而言不存在複雜不複雜這件事情,至少在人工智慧實現之前來說。

所以,反人類的其實是編程所採用的手段和工具,而不是非同步/甚至並發本身。我不反對並發問題本身具有一定的複雜性,但是可以限制在一定範圍內的,而對於直接面對的人類需求而言,大多數都還是人能夠想到和考慮清楚的。不是有句話說,只有想不到,沒有做不到的嗎?既然大多數凡人想像力那麼的局促,為何在滿足他們需求會變的困難呢?即便面對的是千百個人類,也往往只是簡單的疊加而已,畢竟我們的程序還沒有進入到大範圍合作階段。所以,讓並發成為並發本身,不去打斷各個單個事務的連續性,是編程中處理非同步問題的關鍵解決之道。


非同步反人類,可能是因為大多時候的思維都是線性思維吧。但是實際寫一寫發現只要做好handle不會有什麼問題。


因為人類直覺是線性同步的。非同步回調要求把線性的過程割裂,是需要一定的時間適應思維模式的轉變。

然而計算機語言終歸只是符號,多得是辦法可以寫出看起來像同步的非同步。

自然語言也一樣,梗,成句這樣的東西,就跟語法糖一個意思。


如果要證明非同步編程反人類,必須有辦法證明不用非同步編程更人性化。

我剛好寫了一篇文章講解Binding.scala避免非同步編程直接描述業務需求,可以讓需求文檔和代碼逐行一一對應。

有了這種逐行一一對應的能力,那麼一個懂業務的產品經理,只要學一下Binding.scala,不需要懂很深的非同步編程技術就可以逐行把業務需求翻譯成代碼了。

我覺得這樣可能算比較人性化。

More than React系列文章:

《More than React(一)為什麼ReactJS不適合複雜的前端項目?》

《More than React(二)React.Component損害了復用性?》

《More than React(三)虛擬DOM已死?》

《More than React(四)HTML也可以靜態編譯?》

《More than React(五)非同步編程真的好嗎?》

本文首發於InfoQ:More than React(五)非同步編程真的好嗎?

《More than React》系列的上一篇文章《HTML也可以編譯?》介紹了 Binding.scala 如何在渲染 HTML 時靜態檢查語法錯誤和語義錯誤,從而避免 bug ,寫出更健壯的代碼。本篇文章將討論Binding.scala和其他前端框架如何向伺服器發送請求並在頁面顯示。

在過去的前端開發中,向伺服器請求數據需要使用非同步編程技術。非同步編程的概念很簡單,指在進行 I/O 操作時,不阻塞當前執行流,而通過回調函數處理 I/O 的結果。不幸的是,這個概念雖然簡單,但用起來很麻煩,如果錯用會導致 bug 叢生,就算小心翼翼的處理各種非同步事件,也會導致程序變得複雜、更難維護。

Binding.scala 可以用 I/O 狀態的綁定代替非同步編程,從而讓程序又簡單又好讀,對業務人員也更友好。

我將以一個從 Github 載入頭像的 DEMO 頁面為例,說明為什麼非同步編程會導致代碼變複雜,以及 Binding.scala 如何解決這個問題。

DEMO 功能需求

作為 DEMO 使用者,打開頁面後會看到一個文本框。

在文本框中輸入任意 Github 用戶名,在文本框下方就會顯示用戶名對應的頭像。

要想實現這個需求,可以用 Github API 發送獲取用戶信息的 HTTPS 請求。

發送請求並渲染頭像的完整流程的驗收標準如下:

  • 如果用戶名為空,顯示「請輸入用戶名」的提示文字;
  • 如果用戶名非空,發起 Github API,並根據 API 結果顯示不同的內容:
    • 如果尚未載入完,顯示「正在載入」的提示信息;
    • 如果成功載入,把回應解析成 JSON,從中提取頭像 URL 並顯示;
    • 如果載入時出錯,顯示錯誤信息。

非同步編程和 MVVM

過去,我們在前端開發中,會用非同步編程來發送請求、獲取數據。比如 ECMAScript 2015 的 Promise 和 HTML 5 的 fetch API。

而要想把這些數據渲染到網頁上,我們過去的做法是用 MVVM 框架。在獲取數據的過程中持續修改 View Model ,然後編寫 View 把 View Model 渲染到頁面上。這樣一來,頁面上就可以反映出載入過程的動態信息了。比如,ReactJS 的 state 就是 View Model,而 render 則是 View ,負責把 View Model 渲染到頁面上。

用 ReactJS 和 Promise 的實現如下:

class Page extends React.Component {

state = {
githubUserName: null,
isLoading: false,
error: null,
avatarUrl: null,
};

currentPromise = null;

sendRequest(githubUserName) {
const currentPromise = fetch(`https://api.github.com/users/${githubUserName}`);
this.currentPromise = currentPromise;
currentPromise.then(response =&> {
if (this.currentPromise != currentPromise) {
return;
}
if (response.status &>= 200 response.status &< 300) { return response.json(); } else { this.currentPromise = null; this.setState({ isLoading: false, error: response.statusText }); } }).then(json =&> {
if (this.currentPromise != currentPromise) {
return;
}
this.currentPromise = null;
this.setState({
isLoading: false,
avatarUrl: json.avatar_url,
error: null
});
}).catch(error =&> {
if (this.currentPromise != currentPromise) {
return;
}
this.currentPromise = null;
this.setState({
isLoading: false,
error: error,
avatarUrl: null
});
});
this.setState({
githubUserName: githubUserName,
isLoading: true,
error: null,
avatarUrl: null
});
}

changeHandler = event =&> {
const githubUserName = event.currentTarget.value;
if (githubUserName) {
this.sendRequest(githubUserName);
} else {
this.setState({
githubUserName: githubUserName,
isLoading: false,
error: null,
avatarUrl: null
});
}
};

render() {
return (
&
&
&


&
{
(() =&> {
if (this.state.githubUserName) {
if (this.state.isLoading) {
return &{`Loading the avatar for ${this.state.githubUserName}`}& } else {
const error = this.state.error;
if (error) {
return &{error.toString()}&;
} else {
return &;
}
}
} else {
return &Please input your Github user name&;
}
})()
}
& & );
}

}

一共用了 100 行代碼。

由於整套流程由若干個閉包構成,設置、訪問狀態的代碼五零四散,所以調試起來很麻煩,我花了兩個晚上才調通這 100 行代碼。

Binding.scala

現在我們有了 Binding.scala ,由於 Binding.scala 支持自動遠程數據綁定,可以這樣寫:

@dom def render = {
val githubUserName = Var("")
def inputHandler = { event: Event =&> githubUserName := event.currentTarget.asInstanceOf[Input].value }
&
&
&


{
val name = githubUserName.bind
if (name == "") {
&Please input your Github user name& } else {
val githubResult = FutureBinding(Ajax.get(s"https://api.github.com/users/${name}"))
githubResult.bind match {
case None =&>
&Loading the avatar for { name }& case Some(Success(response)) =&>
val json = JSON.parse(response.responseText)
&
case Some(Failure(exception)) =&>
&{ exception.toString }& }
}
}
& }

一共 25 行代碼。

完整的 DEMO 請訪問 ScalaFiddle。

之所以這麼簡單,是因為 Binding.scala 可以用 FutureBinding 把 API 請求當成普通的綁定表達式使用,表示 API 請求的當前狀態。

每個 FutureBinding 的狀態有三種可能,None表示操作正在進行,Some(Success(...))表示操作成功,Some(Failure(...))表示操作失敗。

還記得綁定表達式的 .bind 嗎?它表示「each time it changes」。 由於 FutureBinding 也是 Binding 的子類型,所以我們就可以利用 .bind ,表達出「每當遠端數據的狀態改變」的語義。

結果就是,用 Binding.scala 時,我們編寫的每一行代碼都可以對應驗收標準中的一句話,描述著業務規格,而非「非同步流程」這樣的技術細節。

讓我們回顧一下驗收標準,看看和源代碼是怎麼一一對應的:

  • 如果用戶名為空,顯示「請輸入用戶名」的提示文字;

    if (name == "") {
    &Please input your Github user name&

  • 如果用戶名非空,發起 Github API,並根據 API 結果顯示不同的內容:

    } else {
    val githubResult = FutureBinding(Ajax.get(s"https://api.github.com/users/${name}"))
    githubResult.bind match {

    • 如果尚未載入完,顯示「正在載入」的提示信息;

      case None =&>
      &Loading the avatar for { name }&

    • 如果成功載入,把回應解析成 JSON,從中提取頭像 URL 並顯示;

      case Some(Success(response)) =&>
      val json = JSON.parse(response.responseText)
      &

    • 如果載入時出錯,顯示錯誤信息。

      case Some(Failure(exception)) =&> // 如果載入時出錯,
      &{ exception.toString }& // 顯示錯誤信息。

  • }
    }

結論

本文對比了 ECMAScript 2015 的非同步編程和 Binding.scala 的 FutureBinding 兩種通信技術。Binding.scala 概念更少,功能更強,對業務更為友好。

技術棧ReactJS + Promise + fetchBinding.scala編程範式MVVM + 非同步編程遠程數據綁定如何管理數據載入流程程序員手動編寫非同步編程代碼自動處理能不能用代碼直接描述驗收標準不能能從RESTful API載入數據並顯示所需代碼行數100行25行

這五篇文章介紹了用 ReactJS 實現複雜交互的前端項目的幾個難點,以及 Binding.scala 如何解決這些難點,包括:

  • 復用性
  • 性能和精確性
  • HTML模板
  • 非同步編程

除了上述四個方面以外,ReactJS 的狀態管理也是老大難問題,如果引入 Redux 或者 react-router 這樣的第三方庫來處理狀態,會導致架構變複雜,分層變多,代碼繞來繞去。而Binding.scala 可以用和頁面渲染一樣的數據綁定機制描述複雜的狀態,不需要任何第三方庫,就能提供伺服器通信、狀態管理和網址分發的功能。

如果你正參與複雜的前端項目,使用ReactJS或其他開發框架時,感到痛苦不堪,你可以用Binding.scala一舉解決這些問題。Binding.scala快速上手指南中包含了從零開始創建Binding.scala項目的每一步驟。

後記

Everybody』s Got to Learn How to Code ——奧巴馬

編程語言是人和電腦對話的語言。對掌握編程語言的人來說,電腦就是他們大腦的延伸,也是他們身體的一部分。所以,不會編程的人就像是失去翅膀的天使。

電腦程序是很神奇的存在,它可以運行,會看、會聽、會說話,就像生命一樣。會編程的人就像在創造生命一樣,乾的是上帝的工作。

我有一個夢想,夢想編程可以像說話、寫字一樣的基礎技能,被每個人都掌握。

如果網頁設計師掌握Binding.scala,他們不再需要找工程師實現他們的設計,而只需要在自己的設計稿原型上增加魔法符號.bind,就能創造出會動的網頁。

如果QA、BA或產品經理掌握Binding.scala,他們寫下驗收標準後,不再需要檢查程序員乾的活對不對,而可以把驗收標準自動變成可以運轉的功能。

我努力在Binding.scala的設計中消除不必要的技術細節,讓人使用Binding.scala時,只需要關注他想傳遞給電腦的信息。

Binding.scala是我朝著夢想邁進的小小產物。我希望它不光是前端工程師手中的利器,也能成為普通人邁入編程殿堂的踏腳石。

相關鏈接

  • Binding.scala 項目主頁
  • Binding.scala ? TodoMVC 項目主頁
  • Binding.scala ? TodoMVC DEMO
  • Binding.scala ? TodoMVC 以外的其他 DEMO
  • JavaScript 到 Scala.js 移植指南
  • Scala.js 項目主頁
  • Scala API 參考文檔
  • Scala.js API 參考文檔
  • Scala.js DOM API 參考文檔
  • Binding.scala快速上手指南
  • Binding.scala API參考文檔
  • Binding.scala 的 Gitter 聊天室


先問是不是再問為什麼

原本就是非同步的機制,寫非同步的代碼更容易理解其中的邏輯

為了讓代碼更容易閱讀和維護,才不斷有人開發出攜程、promise、各種數據綁定框架,但這些東西都把實際運行機制隱藏了,如果出了什麼問題需要查錯或者需要更深入定製的時候都更「反人類」


從未聽說搞操作系統的人吐槽軟 / 硬中斷髮人類。


非同步編程是跳躍的,同步是順序的,順序更符合人的思維,也更符合「存儲程序,順序執行」的理論。

用習慣了同步,再用非同步就是難受的,至於用什麼工具都只是苦中作樂,都是量變。


非同步哪裡有反人類了?老闆交給你一堆的事情,你把它們分給下屬去做,都做完了你再和老闆彙報這個叫『反人類』?


因為超出了人類能力範圍。

叫我憋氣10分鐘是反人類吧 然而對一些海洋生物並不算什麼

叫我徒步遠行一千公里反人類吧 一些候鳥表示壓力不大

叫我人腦調度5線程完美協調我表示反我這個人類 隔壁小王表示沒問題不就多寫幾點代碼多修點bug而已 而我只能哀嘆物種間的差異 默默地Google一些非同步庫來彌補我的智力上的短缺


非同步編程就是反人類,同步編程還反計算機類

所以需要人來乾的活,能同步就同步,

需要計算機來乾的活,能非同步就非同步。


推薦閱讀:

程序員真的需要一台 Mac 嗎?
為什麼 Mac 適合編程?
在前端開發中,你曾經遇到過什麼大坑,很久才跳出來?
程序編譯器是否存在這種機制?
如何評價《AWS S3 雲存儲莫名消失:各大網站和 Docker 紛紛中招!》?

TAG:編程 | 非同步 | 反人類 |