文章出處

一、開篇語

  我的上篇文章《關于如何提供Web服務端并發效率的異步編程技術》又成為了博客園里“編輯推薦”的文章,這是對我寫博客很大的鼓勵,也許是被推薦的原因很多童鞋在這篇文章里發表了評論,有童鞋說我這篇文章理論化很嚴重,沒有實際代碼和具體項目做支撐,這個評論讓我有種理論和實踐脫節的味道,所以我想在這里談談我為什么要寫這篇文章的原因,這篇文章是把我前不久學習多線程編程的一個總結。

  當我從我書堆里找到所有與多線程開發相關的書籍簡單閱讀后,我發現了一個問題,在java里開發多線程最強有力的實踐就是做服務端的并發處理,其他的應用都是小兒科,而這個實踐對于每個web程序員而言都是極具經濟價值的,所以我就想延著這個實踐繼續學習多線程,接著我發現并發處理這塊都離不開IO處理,同時IO處理的優劣直接關系著并發處理的效率問題,    關于并發的知識,有人說最好是寫寫代碼,告訴我們API如何運用,說實話簡簡單單寫幾行不是太難的事情,寫幾行代碼并不代表你能掌握它,掌握多線程絕不是集中精力看懂幾行代碼,看懂后頓悟的來一下:哦,明白了。

  要真的掌握某種技術你就必須要知其所以然,但是要理清并發的所以然就不那么容易了,這其實是一種設計的思想,而不是簡單幾行代碼,幾個jdk提供的api,代碼和api只不過是實踐這些思想的實現,我之前學習線程開發,學習io的api,這個過程很早就開始,但這個過程都基本都是記住遺忘再記住再遺忘的死循環,究其原因就是因為它們本身的設計思想太深奧,你沒有理解這個設計思想,你就無法將那些api的內在關系串聯起來,所以最終的結果只會是遺忘。技術的本質就是工具,要用好工具的前提就是要真正理解這個工具的價值,這就好比斧頭設計出來時用來砍柴的,菜刀設計出來是為了切菜,如果我們用斧頭用來切菜,菜刀用來砍柴,這肯定要鬧笑話了,對于像線程和IO這樣復雜的工具,使用起來和使用像這種簡單工具菜刀和斧頭一樣,應該是物盡其才,各司其職,復雜工具的麻煩就是理解起來比較困難些了,自己得多花點時間和功夫了。

  我早期的文章都會寫好多代碼,但是最近的文章我會寫更多文字,我希望對我熟悉的技術有更進一步的理解,因此理論和猜想會更多點。

  本篇文章的標題是《關于Web開發里并發、同步、異步以及事件驅動編程的相關技術》,本文的目的就是要和大家一起好好的理解下標題里很常見的概念,通過理解它們,指導我們今后對這些概念的學習和運用。

二、我們來好好理解下并發、同步和異步這三個概念

  標題的三個概念在web開發領域太常見了,就算是入門web開發的童鞋隨便找本web開發的書籍都能找到這三個概念,但是如果你對這三個概念理解不夠透徹,如果在實際開發中正好碰到三個概念的交織在一起的問題,估計很多人就會麻了頭。

  并發

  首先是并發這個概念,我們看看現實中并發的例子。

  第一個例子:

  有一天你在家里做飯,你這天的心情非常好,想大吃一頓,所以要有菜有湯還有酒喝,開始干了,你首先會在電飯煲里把飯煮好,飯在煮的時候,你會抽空去超市買瓶啤酒,買好了啤酒回到家里你就要做菜了,你想做個湯,為了節約時間,你會先燒開水,燒開水同時,你會切菜,切好的菜就得炒菜,后面的過程此處省略一萬字了,地球人我想都明白以后是咋樣了,我就不再詳細講述了。 

  上面的描述里煮飯、燒水、買啤酒、切菜、炒菜,它們都可以當作獨立的任務,這些任務都是為了達到某種特定的目的,它們之間是不能相互替代替換的,但是在實際操作里你并不是等待某個任務做完了再去做另一個任務,而是會運用統籌方法,交叉進行,這么做的好處就是效率很高,節約時間,這和程序員平時工作類似,一邊編寫程序,一邊聽歌,一邊和朋友同事在QQ里聊天。

  第二個例子:

  現實生活里還有一種并發,例如我們在食堂里吃飯,去食堂吃飯的人很多,因此食堂開啟了好幾個打飯的窗口,如果打飯的窗口開的越多,那么吃飯人打到飯所用時間也就最少,如果每個食客都有一個窗口一對一的服務,那么食客基本就不用浪費任何時間去等待了,這種vip感在中國這么資源緊張的國家里,簡直太爽了,食堂打飯的并發就和我上篇文章里講到的web服務端并發類似。

  這里有列舉了兩個并發的實例,似乎有點多此一舉之嫌,但是如果我們仔細研究下這兩個例子,它們其實有本質的不同,第一個例子其實是一個人的并發,第二個例子就是多人的并發,用計算機的概念描述就是前一個例子就是一個線程完成的并發,后一個例子就是多線程完成的并發,第一個例子的線程載體就是做飯的人,第二個線程的載體就是每個打飯窗口的工作人員,這兩個例子其實就可以類比到我上篇文章提到的做法二和做法三,特別是第一個例子,雖然這個場景人們非常熟悉,但是到了程序開發里卻有很多人把第一個實例并發給遺忘掉,特別是做web開發的人,并發就是多線程,一個線程怎么可能做并發了,但是例子一說明一個線程是可以做并發的。

  由上面的描述,下面我要給出我自己總結的并發定義:

 并發就是讓一系列獨立的任務同時執行,同時執行包含兩種情形,一種就是完全獨立執行,例如在擁有雙核處理器的計算機里,每個CPU在同一個時間里處理不同的任務,另一種情形是一個任務還沒執行完,而CPU計算被閑置的時候CPU用來處理別的任務。

  而現實里我們要做到高效的并發就得把兩種模式的并發混合起來使用,食堂打飯就是混合了兩種模式的并發,對于計算機兩種混合的并發才能把計算機的計算能力深度挖掘,所以做法三就是混合了兩種并發,做法三是高效和先進的,做法三是值得推廣的。

  接下來就要談談同步和異步兩個概念了,記得剛工作不久,有次參加面試,面試官問我什么是同步什么是異步,我當時的回答是:同步就是單線程,異步就是多線程,當時那位面試官停頓了下,跟我說這個解釋沒有問題,但是在實際開發里這個答案不能讓你把同步和異步用的很好。現在回憶那位面試官給我答案的評定還是有道理的,下面我開始講同步和異步了。

  同步:

  在計算機領域同步應該是指相互獨立的任務一個接著一個運行,拿上面做飯的例子描述就是煮飯煮好了才能去超市買啤酒,酒買好了才能切菜,菜切好了才能炒菜,菜炒好了才能燒水燒湯,明顯同步的方式在現實生活里效率是很低的,不過同步有個很大的好處就是簡單,簡單的理論基礎就是一次就做一件事情,做的時候一心一意專心致志。

  異步:

  下面就是異步的概念,同步和異步在當代的計算機領域里是兩個對等的概念,但是如果我們要追溯異步的本質,就會發現同步和異步其實是一個因果關系,同步是因,異步是果,換個說法同步就是異步這個孩子他娘,所以我們要正確理解異步就要明白同步做不好的事情,在web前端有個鮮活的例子可以說明這個問題,這個例子就是ajax,在ajax出現前,瀏覽器的一個獨立頁面和服務端的交互是通過一個socket進行的,這個socket相當于一個線程,它是采用同步處理的方式,所以沒有ajax的時代,單獨頁面每次和服務端的交互都只能做一件事情,而網絡處理的速度往往是最慢,而且網絡的效率很容易受到外界影響,因此同步請求會導致用戶使用頁面的體驗很不好,同步網站時刻都在考驗著用戶的耐心,這就導致ajax的出現,ajax本質就是瀏覽器提供了一個新的socket鏈接,這個socket是有別于同步的socket,它可以獨立于同步socket運行,有了ajax這個socket我們就可以在不影響頁面同步操作的前提下也能從服務端獲取數據,這就好比瀏覽器由原來一個人完成和服務端的交互變成了現在兩個人完成和服務端的交互操作,它們各司其職,共同完成頁面上的功能。

  異步和并發的關系和區別:

  異步操作和并發看起來很像,特別是和我前面講到并發的做飯實例很像,的確并發和異步常常是交織在一起的,但是它們還是有很大的區別,這個區別在于它們所達到的業務目的,并發是業務含義是能做更多事情,而異步是讓多個人共同完成一個任務,異步其實是通過專業角度把一個大任務拆分成相對獨立小任務,讓更加專業的人完成這個小任務,小任務完成后最后匯總成一個大任務的結果,上面ajax就是這樣的道理,其實我以前著重研究的hadoop就是一個典型的異步任務系統。

  異步和并發共同點都是通過多線程來實現,通過它們在業務場景的區別,我們反過來學習多線程,就知道多線程能為我們做好哪些事情,那么當你碰到需要使用多線程的業務場景就知道按什么思路來分析這個業務場景了。

三、關于事件驅動編程

  在我上篇文章里我反復寫了好多事件驅動這四個字,到了本文里事件驅動后面我加了兩個字編程,為什么加它我后面會講到。

  全世界最熟悉事件驅動的程序員是哪種程序員?答案是前端工程師,不管是桌面前端還是web前端都是世界上最熟悉事件驅動的,以web前端為例,我們做頁面可以不去想什么面向對象編程,什么jQuery框架咋用,但是為按鈕,為頁面元素添加相關事件操作肯定是不可缺少的,而web前端的事件處理機制就是標準的事件驅動機制,為了講清楚事件驅動,這里我回顧下頁面里事件機制,我們開發頁面的事件時候,第一步就是定義事件(定義事件就是在定義一個函數)或者說為事件定義一個動作,并把事件綁定到指定的元素上,如果我們沒有觸發元素上的事件,那么定義好的函數也就不會執行,如果元素上的事件被觸發了,定義好的函數才會執行。代碼不提供了,這個太簡單和平常了。

  關于瀏覽器里事件機制實現方案,我找了許久都沒有找到完整的資料,因此這里我大膽揣測下事件機制的實現方案,下面的內容完全是我的猜想,不一定和實際相符,具體如下:

  首先我要說多線開發程里有一個經典的設計方法,這個方法就是生產-消費者模型,生產-消費模型特點就是生產者和消費者被一個中間隊列分隔開來,不管是生產請求還是消費結果都是通過這個中間隊列中轉,這樣就可以把生產者和消費者關系解耦,事件實現機制從宏觀上和生產-消費模式類似,這個類似不是指設計思想,而是溝通雙方聯系的那個中間層。

  事件處理的機制里應該有個事件處理器,事件處理器位于元素和事件處理方法的中間位置,我們在定義事件的時候就是等于在事件處理器里定義元素和事件處理方法的關系,當這種對應關系定義好后,事件處理器就會啟動一個死循環,這個循環反復檢測元素的狀態變化,當事件處理器發現某個狀態產生了變化,處理器就會找到對應的事件處理方法,然后執行這個方法。

  Nodejs是一個事件驅動的語言,這是官方對nodejs的定義,很多評論說nodejs是第一個把事件驅動上升到語言層級的編程語言。所以本文我在事件驅動后面加上了編程兩字。

傳統語言做開發都是按時間先后順序進行的,這么做既可以降低語言的學習成本,也讓開發代碼思路比較容易控制,但是現實場景是復雜的,這種按事件順序的開發流程并不一定是我們解決現實問題的最佳方式。這好比我們做一件事情,在做的時候我們會碰到很多情況,由于發生的情況的不同,那么這件事情的結果可能就會因為情況不同而發生變化,如果按照時間順序的編程方式想做好上面的事情會讓程序變得十分復雜,因為我們要按照時間順序做出各種不同執行路徑,這就是排列組合的辦法了,這顯然讓事情變得復雜了,如果用事件驅動編程方式,我們只要定義好事務的起因,各種不同的過程情況,以及所能得到結果,換句話說我們首先只要關注實體內容而忽略事務關系問題,而事務關系則是在事件處理器里定義的,當我們發送給事件處理器一個指示信號,處理器就會對應找到某個行為,那么事件驅動編程就簡化了程序開發的流程。

  事件驅動編程實現的核心技術就是能讓方法變成對象能在事件處理的流程里傳遞,方法得到事件管理器的指令后在合適的位置上被促發,這就是回調函數,而javascript語言里函數可以當做對象傳遞,也就保證了事件驅動編程上升到語言層級變成了可能,我想這就是nodejs作者使用google的V8引擎設計出nodejs的重要原因之一。

  回調函數改變了傳統程序開發的流程,但是大量使用回調函數的代碼常常會變得晦澀難懂,這也是javascript語言很掌握的重要原因之一。鑒于回調函數這個毛病出現了promise編程,promise的目的是讓回調編程看起來像按時間順序編程的方式,我前端時間研究了下promise,但是沒有深究,原因是回調本來就很難理解,回調變成順序編程,那豈不是更加糊里糊涂,按現在技術特點,我會選擇慎用promise技術。

四、小結

  本文的主要內容就到此為止,鑒于上篇文章有些內容有很大的爭議,本文想做一定的解釋說明:上文最大的問題還是IO的解釋上,我承認自己對IO其實理解不太深入,所以我只是用文字描述非阻塞IO的處理,這段文字寫的時候我還是很注意的,盡量不講太多,我當時只用一個理念來寫這個實現,就是非阻塞IO的具體實現里一定會有一個和事件處理器相類似的中間層來協調IO操作和CPU的操作,這點我自信不會有錯。其實IO技術在java里相當復雜的,比較難學,現在jdk提供的IO的模型有三大類,BIO(阻塞IO),NIO(阻塞IO或者叫新IO)以及AIO(異步IO或者叫NIO2.0),jdk的io是建立在操作系統IO上的,所以學習IO真的要多花點心思和精力,這是我今后學習研究的一個重點。

  此外,在計算機里不管執行什么任務都會包含IO操作和CPU計算兩個過程,IO的速度問題常常拖累了CPU的計算,由于某些IO太慢了,如果站在CPU的角度,它等待IO處理的代價實在太高了,所以先進的IO就是為了調整IO處理和CPU計算的關系,我覺得新IO解決方案要解決的核心問題。

  好了,本文寫到這里,本文和上文類似都是談自己對某些技術的理解,文字很多,幾乎沒啥代碼,希望童鞋們多拍拍磚,有問題才能進步的更快。

 

 


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()