為什麼說非同步編程是反人類?
callback,promise,await,就越來越人性化了。如果你有一個好的非同步庫,你會發現寫非同步代碼比寫同步代碼在處理並發之類的問題上簡單多了,幾乎從來不用考慮加鎖的問題。比如說可以試試VLCPhubo1016/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不會有什麼問題。
因為人類直覺是線性同步的。非同步回調要求把線性的過程割裂,是需要一定的時間適應思維模式的轉變。
然而計算機語言終歸只是符號,多得是辦法可以寫出看起來像同步的非同步。
自然語言也一樣,梗,成句這樣的東西,就跟語法糖一個意思。如果要證明非同步編程反人類,必須有辦法證明不用非同步編程更人性化。
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}`}&
const error = this.state.error;
if (error) {
return &{error.toString()}&
} else {
return &;
}
}
} else {
return &Please input your Github user name&
}
})()
}
&