此次來聊聊clojure的並行與併發,若是你還不知clojure爲什麼物,請翻翻個人上一篇推文。「並行」是指clojure對並行計算的支持(parallel computing),「併發」是其併發特性(concurrency)。用通俗的話來講,「並行」是同一時間作多件事情,「併發」是同一時間應對多件事情。舉個例子,「並行」就相似於GPU作3D繪圖,左手畫圓、右手畫方;「併發」就相似於web 服務器利用服務器的多個內核來同時處理來自用戶的多個請求。若是還不夠明白,請你們google一下wiki。^_^java
Clojure對並行計算支持的很好,而clojure的併發性其實一篇文章都寫不完,由於它頗有特點。clojure沒有提供傳統併發編程的元素,如:線程和鎖。但clojure卻提供了與線程和鎖無關的、徹底不一樣的4種併發編程模型,儘管你能夠理解爲這是clojure這麼函數式語言基於java線程和鎖的抽象。android
Clojure對並行計算的支持主要是經過並行類庫與函數來提供支持,例如:reducer、pmap、pvalues、pcalls等,要注意的是:適用於CPU密集型任務,不是I/O密集或block的情形。reducer你沒看錯,的確是大數據的map-reduce的reducer,他們有聯繫?沒有,但的確你能夠把Clojure的並行計算包clojure.core.reducers用於大數據處理。想象一下,若是你須要處理一個大約 40G 的文件,對每一行文本進行解析並執行一些計算邏輯,最後寫入數據庫。你要怎麼實現?是否是想到分而治之,但文件IO可能又是瓶頸,如何分割文件,而後放到內存,用clojure.core.reducers包來並行處理呢? 這是一個思路:先map後reduce。好了,回到正題。web
舉個栗子:Java如何對一個數列求和,代碼是否是這樣的:docker
public int sum(int[] numbers){數據庫
int accumulator = 0;
編程
for(int n: numbers)json
accumulator += n;
瀏覽器
return accumulator;安全
}服務器
Clojure是這樣寫:
(defn reduce-sum [numbers]
(reduce (fn [acc x] (+ acc x)) 0 numbers))
這段代碼用了clojure的reduce函數,其中3個參數:一個化簡函數、一個初始值、一個集合。先用fn定義了一個匿名函數,接受兩個參數並返回參數之和。而後後面的就是初始值和集合。其實,clojure有一個現成的函數+帶代替fn這個匿名函數,因此代碼能夠繼續簡化:
(defn sum [numbers]
(reduce + numbers))
上面都尚未涉及並行計算,如今,咱們引入並行庫clojure.core.reducers包,用裏面的fold函數替換reduce:
(ns sum.core
(:require [clojure.core.reducers :as r]))
(defn parallel-sum [numbers]
(r/fold + numbers))
測試一下1加到一億,性能提高2.5倍。背後是什麼魔法?你們能夠看clojure的官方說明和源碼,它底層是用了JVM 原生的 fork/join 工具而進行的優化,看源碼它默認起的java線程數爲n+2(爲何?照例cpu密集型的線程池配置應該是對cpu密集型的爲等於cpu數,I/O密集型的爲更多),其主要實現思路:
分而治之(Partitioning the reducible collection at a specified granularity (default = 512 elements))
應用到各個分區(Applying reduce to each partition)
分區計算結果集合並(Recursively combining each partition using Java's fork/join framework.)
關於pmap等,此處不展開了。
前面提到:clojure沒有提供傳統併發編程的元素,如:線程和鎖。但clojure卻提供了與線程和鎖無關的、徹底不一樣的4種併發編程模型,儘管你能夠理解爲這是clojure這麼函數式語言基於java線程和鎖的抽象。這4種併發編程模型位:
線程本地vars(thread-local)
原子變量(atoms)
代理(agent)
引用(refs)和軟件事務內存(ATM)
Clojure中全部的數據都是非易變的,除非用相應的Var、Ref、Atom和Agent類型明確表示某數據是易變的。這提供了管理共享狀態的安全機制,對於這一點要深入理解。而對於上面這4種併發編程模型,筆者在仔細研究以後發現,最簡潔適用的是第二種,因此筆者具體展開第二種原子變量,其它幾種有興趣的朋友本身去研究,我想理解了第二種其它的應該都容易理解。
原子變量實際上是在java.util.concurrent.atomic的基礎上創建的。而java.util.concurrent.atomic背後其實用了CPU指令來實現的原子性保證,並使用了java.util.concurrent.atomicReference包提供的compareAndSet f方法,即CAS樂觀比較重試法,因此內部沒有鎖。但java用cas也避免不了重試而clojure的原子變量爲什麼能避免重試呢?緣由就在於Clojure是函數式語言,其原子變量是無鎖的,由於Clojure中全部的數據都是非易變的,是常量,它的值不是變化的,而是其數據結構被修改時老是保存了其以前的版本。
舉個栗子,用原子map:
(def test (atom {}))
(swap! test assoc :username "paul")
(swap! test assoc :id 123)
再舉一個管理運動員的web服務,這個代碼有點多,但很好理解:
(def players (atom ()))
(defn list-players []
(response (json/encode @players)))
(defn create-player [player-name]
(swap! players conj player-name)
(status (response "") 201))
(defroutes app-routes
(GET "/players" [] (list-players))
(PUT "/players/:play-name" [player-name] (create-player player-name)))
(defn -main [& args]
(run-jetty (site app-routes) {:port 3000}))
常常有朋友問我如何自我提升,學習新知識?其實咱們這一行要學習的東西真的不少,以前作無線的時候我還要學習客戶端的東西(android/iOS/H5)和產品經理/UX的知識,如今要學docker看它的源碼就要學Go語言。活到老,學到老嘛,stay hungry, stay foolish,永遠把本身當成初學者,這樣才能對新事物保持好奇,對全部觀點持開放態度。
我建議的學習方法:
閉環學習:從瀏覽器、網絡協議、webserver、數據庫一個閉環,你都有了解嗎?長的閉環鏈條能賦予你全面分析和解決問題的能力,很容易定位和分析問題,也有了本身的知識體系。
順藤摸瓜:好比Socket -> UNIX網絡編程 -> TCP/IP協議,順藤摸瓜,每每會發現本身研究的越多,不懂的越多。才發現知道了本身不知道....
量變到質變:學了不少,實踐了嗎?學以至用,沒用,就真的沒用了。量變造成質變,沒量不可能質變,代碼沒寫幾行?寫了一萬行沒總結提煉和思考也不可能質變。就像咱們今天學了clojure的是否能夠總結一下各類併發編程模型? 多實踐、多思考。
固然,前提是有興趣,沒有興趣學習的話早點轉行。
(原文發佈與微信公衆號 rayisthinking, 原文連接:http://mp.weixin.qq.com/s?__biz=MzAxNTQ4NTIzNA==&mid=208631556&idx=1&sn=d404833e167dc46868a26f43d09187a1#rd)