并發(fā)

2018-02-24 15:48 更新

現(xiàn)代服務是高度并發(fā)的—— 服務器通常是在10–100秒內(nèi)并列上千個同時的操作——處理隱含的復雜性是創(chuàng)作健壯系統(tǒng)軟件的中心主題。

線程提供了一種表達并發(fā)的方式:它們給你獨立的,堆共享的(heap-sharing)由操作系統(tǒng)調(diào)度的執(zhí)行上下文。然而,在Java里線程的創(chuàng)建是昂貴的,是一種必須托管的資源,通常借助于線程池。這對程序員創(chuàng)造了額外的復雜,也造成高度的耦合:很難從所使用的基礎資源中分離應用邏輯。

當創(chuàng)建高度分散(fan-out)的服務時這種復雜度尤其明顯: 每個輸入請求導致一大批對另一層系統(tǒng)的請求。在這些系統(tǒng)中,線程池必須被托管以便根據(jù)每一層請求的比例來平衡:一個線程池的管理不善會導致另一個線程池也出現(xiàn)問題。

一個健壯系統(tǒng)必須考慮超時和取消,兩者都需要引入更多“控制”線程,使問題更加復雜。注意若線程很廉價這些問題也將會被削弱:不再需要一個線程池,超時的線程將被丟棄,不再需要額外的資源管理。

因此,資源管理危害了模塊化。

Future

使用Future管理并發(fā)。它們將并發(fā)操作從資源管理里解耦出來:例如,F(xiàn)inagle(譯注:twitter的一個RFC框架)以有效的方式在少量線程上實現(xiàn)并發(fā)操作的復用。Scala有一個輕量級的閉包字面語法(literal syntax),所以Futures引入了很少的語法開銷,它們成為很多程序員的第二本能。

Futures允許程序員用一種可擴充的,有處理失敗原則的聲明風格,來表達并發(fā)計算。這些特性使我們相信它們尤其適合在函數(shù)式編程中用,這也是鼓勵使用的風格。

更愿意轉(zhuǎn)換(transforming)future而非自己創(chuàng)造。Future的轉(zhuǎn)換(transformations)確保失敗會傳播,可以通過信號取消,對于程序員來說不必考慮Java內(nèi)存模型的含義。甚至一個仔細的程序員會寫出下面的代碼,順序地發(fā)出10次RPC請求而后打印結(jié)果:

 val p = new Promise[List[Result]]
 var results: List[Result] = Nil
 def collect() {
   doRpc() onSuccess { result =>
     results = result :: results
     if (results.length < 10)
       collect()
     else
       p.setValue(results)
   } onFailure { t =>
     p.setException(t)
   }
 }

 collect()
 p onSuccess { results =>
   printf("Got results %s\n", results.mkString(", "))
 }

程序員不得不確保RPC失敗是可傳播的,代碼散布在控制流程中;糟糕的是,代碼是錯誤的! 沒有聲明results是volatile,我們不能確保results每次迭代會保持前一次值。Java內(nèi)存模型是一個狡猾的野獸,幸好我們可以通過用聲明式風格(declarative style)避開這些陷阱:

 def collect(results: List[Result] = Nil): Future[List[Result]] =
   doRpc() flatMap { result =>
     if (results.length < 9)
       collect(result :: results)
     else
       result :: results
   }

 collect() onSuccess { results =>
   printf("Got results %s\n", results.mkString(", "))
 }

我們用flatMap順序化操作,把我們處理中的結(jié)果預追加(prepend)到list中。這是一個通用的函數(shù)式編程習語的Futures譯本。這是正確的,不僅需要的樣板代碼(boilerplate)可以減少,易出錯的可能性也會減少,并且讀起來更好。

Future組合子(combinators)的使用。當操作多個futures時,F(xiàn)uture.select,F(xiàn)uture.join和Future.collect應該被組合編寫出通用模式。

集合

并發(fā)集合的主題充滿著意見、微妙(subtleties)、教條、恐懼/不確定/懷疑(FUD)。在大多實際場景都不存在問題:總是先用最簡單,最無聊,最標準的集合解決問題。 在你知道不能使用synchronized前不要去用一個并發(fā)集合:JVM有著老練的手段來使得同步開銷更小,所以它的效率能讓你驚訝。

如果一個不可變(immutable)集合可行,就盡可能用不可變集合——它們是指稱透明的(referentially transparent),所以在并發(fā)上下文推斷它們是簡單的。不可變集合的改變通常用更新引用到當前值(一個var單元或一個AtomicReference)。必須小心正確地應用:原子型的(atomics)必須重試(retried),變量(var類型的)必須聲明為volatile以保證它們發(fā)布(published)到它們的線程。

可變的并發(fā)集合有著復雜的語義,并利用Java內(nèi)存模型的微妙的一面,所以在你使用前確定你理解它的含義——尤其對于發(fā)布更新(新的公開方法)。同步的集合同樣寫起來更好:像getOrElseUpdate操作不能夠被并發(fā)集合正確的實現(xiàn),創(chuàng)建復合(composite)集合尤其容易出錯。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號