莊周夢蝶

          生活、程序、未來
             :: 首頁 ::  ::  :: 聚合  :: 管理


              這個問題的由來是有一個朋友報告xmemcached在高并發下會頻繁斷開重連,導致cache不可用,具體請看這個issue

              我一開始以為是他使用方式上有問題,溝通了半天還是搞不明白。后來聽聞他說他的代碼會中斷運行時間過長的任務,這些任務內部調用了xmemcached跟memcached交互。我才開始懷疑是不是因為中斷引起了連接的關閉。

              我們都知道,nio的socket channel都是實現了 java.nio.channels.InterruptibleChannel接口,看看這個接口描述:

          A channel that can be asynchronously closed and interrupted.

          A channel that implements this interface is asynchronously closeable: If a thread is blocked in an I/O operation on an interruptible channel then another thread may invoke the channel's close method. This will cause the blocked thread to receive an AsynchronousCloseException.

          A channel that implements this interface is also interruptible: If a thread is blocked in an I/O operation on an interruptible channel then another thread may invoke the blocked thread's interrupt method. This will cause the channel to be closed, the blocked thread to receive a ClosedByInterruptException, and the blocked thread's interrupt status to be set.

          If a thread's interrupt status is already set and it invokes a blocking I/O operation upon a channel then the channel will be closed and the thread will immediately receive a ClosedByInterruptException; its interrupt status will remain set.


              意思是說實現了這個接口的channel,首先可以被異步關閉,阻塞的線程拋出AsynchronousCloseException,其次阻塞在該 channel上的線程如果被中斷,會引起channel關閉并拋出ClosedByInterruptException的異常。如果在調用 channel的IO方法之前,線程已經設置了中斷狀態,同樣會引起channel關閉和拋出ClosedByInterruptException。

               回到xmemcached的問題,為什么中斷會引起xmemcached關閉連接?難道xmemcached會在用戶線程調用channel的IO operations。答案是肯定的,xmemcached的網絡層實現了一個小優化,當連接里的緩沖隊列為空的情況下,會直接調用 channel.write嘗試發送數據;如果隊列不為空,則放入緩沖隊列,等待Reactor去執行實際的發送工作,這個優化是為了最大化地提高發送效率。這會導致在用戶線程中調用channel.write(緩沖的消息隊列為空的時候),如果這時候用戶線程中斷,就會導致連接斷開,這就是那位朋友反饋的問題的根源。

                Netty3的早期版本也有同樣的優化,但是在之后的版本,這個優化被另一個方案替代,寫入消息的時候無論如何都會放入緩沖隊列,但是Netty會判斷當前寫入的線程是不是NioWorker,  如果是的話,就直接flush整個發送隊列做IO寫入,如果不是,則加入發送緩沖區等待NioWorker線程去發送。這個是在NioWorker的 writeFromUserCode方法里實現的:
           

              
          void writeFromUserCode(final NioSocketChannel channel) {
                  
          if (!channel.isConnected()) {
                      cleanUpWriteBuffer(channel);
                      
          return;
                  }

                  
          if (scheduleWriteIfNecessary(channel)) {
                      
          return;
                  }

                  
          // 這里我們可以確認 Thread.currentThread() == workerThread.
                  if (channel.writeSuspended) {
                      
          return;
                  }

                  
          if (channel.inWriteNowLoop) {
                      
          return;
                  }
                  write0(channel);
              }
             
              我估計netty的作者后來也意識到了在用戶線程調用channel的IO操作的危險性。xmemcached這個問題的解決思路也應該跟Netty差不多。但是從我的角度,我希望交給用戶去選擇,如果你確認你的用戶線程沒有調用中斷,那么允許在用戶線程去write可以達到更高的發送效率,更短的響應時間;如果你的用戶線程有調用中斷,那么最好有個選項去設置,發送的消息都將加入緩沖隊列讓Reactor去寫入,有更好的吞吐量,同時避免用戶線程涉及到 IO操作。

          posted @ 2010-08-18 18:08 dennis 閱讀(4893) | 評論 (0)編輯 收藏

          非終稿,有興趣先看看。

          posted @ 2010-08-17 18:07 dennis 閱讀(4127) | 評論 (3)編輯 收藏

              忘了啥時候聽說盛大的電子書bambook要搞內測,然后某天抱著試一試的心態,翻出許久未用的起點帳號,一看我竟然還是04年注冊的高級VIP用戶,想起來那時候好像是為了看一些網絡小說特意注冊的。然后申請了內測邀請碼,還預先存了50塊起點幣。內測開始那天是9號吧,中午2點多翻了下短信,沒想到我運氣不錯,成了第一批bambook內測用戶。哇卡卡,趕緊下單,付款的時候費了點功夫,用信用卡付不了,只好下午下班后存了些錢到工資卡,付款成功已經是晚上7點了。
              接下來是漫長的等待,本以為上海到杭州最多兩天,沒想到查看訂單狀態一直停留在配貨狀態,這讓習慣了京東神速的我很著急,不過看起來盛大文學還是一直在改進這個訂單系統,后來將宅急送的訂單查詢也融合到訂單里面,用戶可以查看自己的東西當前在哪里。3天后終于收到貨,中間發生了一個小插曲,我發現訂單狀態變成了完成,顯示我已經簽收,而我實際上沒有收到快遞公司的電話,更何況簽收?打電話給客服,客服叫我別著急,他們先查查。后來才知道公司是統一代收快遞,小郵局代收了,給我發了郵件才知道。盛大的客服還是很負責任的,后來打電話向我確認情況才知道是虛驚一場,實在不好意思。物流這塊,我覺的盛大這次內測做的并不好,看看bambook官方論壇那么多抱怨就知道,發貨太慢,并且客服的反饋都是復制粘帖,一律說什么產品銷售火爆,發貨快不起來的鬼話。這是內測呀,才發多少臺,只能說前期準備不是特別充分。

              先看照片,我兒子很喜歡bambook







               外觀我覺的還是很漂亮,我沒有用過其他的電子書產品,bambook給我的感覺還不錯,屏幕是6寸的電子墨水屏,還有一些快捷鍵方便翻頁、查看書架等,還有一個搖桿作為確認鍵,也可以翻頁以及調整選項等。背面是奧運火炬的祥云圖案,這些網上已經有很多評測照片了,我就不多說了。從硬件上,bambook還是很讓我滿意的。不過沒有提供閱讀燈挺讓人遺憾,不知道會不會作為配件提供,thinkpad的鍵盤燈是非常棒的設計,如果能有閱讀燈,那就可以不打擾家人讀書了。

              這是我第一次接觸電子墨水屏,曹老大跟我說會有殘影的問題,入手后發現果然有這個現象,但是不細看還是很難看出來,對閱讀沒有任何影響。相反,這種屏幕對讀書還是極好,在陽光下也可以正常閱讀,并且只有在有變化的時候費電,非常省電,我拿到手連續看了兩天小說,電池顯示還至少一格電。通過云梯——bambook提供的客戶端軟件,可以修改字體,修改成微軟雅黑,終于不用捧著個筆記本在床上看書。

              bambook至少提供了3種上網方式:USB連接PC代理上網、WIFI以及3G網絡。目前3G卡還不行,USB我沒測試。公司和家里都是無線網絡,在公司可以正常連接,但是在家里卻不行。后來發現這是因為bambook無法輸入SID,而我家的SID廣播是禁止掉的,為了讓bambook能聯網,只好修改路由器配置了。這一點不是很好,對家庭用戶來說,通常還是會禁止SID廣播。bambook上網只能連接盛大的云中書城,包括起點、榕樹下等盛大文學下面的網站。這些網站大多數是一些網絡文學作品,我嘗試搜索一些文史類的讀物都沒有找到。網絡小說我還是會讀,比如最近又重新看《慶余年》(bamboook里這本書是免費的),但是買bambook可不僅僅是為了讀網絡小說。還是希望云中書城能多一些其他方面的書籍,不過內容建設是個長期的過程,倒是不著急,畢竟還在內測階段。

             bambook允許傳入自有書籍,但是需要轉換成它的內部測試,利用云梯這個客戶端軟件。云梯目前只支持windows,我的機器是ubuntu,不過嘗試了下在virtualbox虛擬機上的XP里也可以正常使用。我嘗試轉換了txt和pdf文件,都可以正常轉換,比較離奇的是txt轉換后的文件還更大。PDF這是重點,我還是想在bambook上閱讀一些技術文檔,這些文檔大多數是PDF格式。遺憾的是bambook對PDF轉換的支持并不完善,章節索引沒了不說,轉換后的格式在6寸屏幕上的排版很糟糕,諸如圖表、代碼之類的跟原文檔比起來很不適合閱讀。事實上,我是很不理解為什么要自定義格式,如果說是為了保護商業版權,但是你又允許上傳自由書籍,自由書籍還需要經過這么一個轉換步驟,還不如原生支持一些常見格式。bambook的os是基于anroid開發的,我相信原生支持PDF這樣的常見格式肯定不是特別大的問題。pdf文檔這個問題在官網上也有很多討論,從客服的反饋來看是有計劃改進,拭目以待吧。

             接下來說說我認為bambook軟件上可以改進的一些小地方,首先是剛才提到的網絡連接問題,允許用戶輸入隱藏的SID。網絡在不用一段時間后會自動斷開,這個時間我估計了下是10-15分鐘左右,但是沒有找到可以這個時間的地方,這個選項還是應當開放給用戶來選擇。字體上我也希望能自定義字體,目前是預先定義了幾個大類,你只能在這幾個大類里選,我還是希望能輸入數值自定義字體大小,好像字體的設置還有個Bug,設置的字體不會保存,在下次繼續閱讀的時候還需要重新設置。另外,就是那些提示框比較傻,非要我點下確認才消失,提示框應當在幾秒內自動消失,這對用戶體驗更好。書架也應當允許分類,現在是一股腦堆在一起,幾本書還好,多了就比較麻煩了。書架的更新和內容的更新應當分離,有時候我只是想更新下書架,并不想更新內容。
             硬件上我希望盡快出個套套,這東西白的稀里嘩啦,很容易弄臟和摔壞,有個套套比較好,最好套套上能再帶個閱讀燈那就更好了。充電方式也應當再多樣化一點,目前只能連到PC充電,并且是無論如何都要會自動充電,應當讓用戶決定連接的時候是否充電才對。云梯在格式轉換上還要下更大功夫才行,當然,我更希望是直接原生支持PDF了,不過從一個盛大的朋友那了解到這是比較困難的。

             總之,內測998這個價,bambook還是物有所值的,硬件上很好,軟件上問題不少,內容建設上還有很多要做的事情,例如我希望以后每天早上能直接用bambook看當天的報紙,哪怕是要錢訂閱的。在淘寶上查了下,bambook內測這個版本轉手也能賣到1200甚至更多,還是挺搶手的。慶幸的是bambook可以刷固件,等哪天升級的時候,希望這些軟件上的小問題能有個比較好的解決方案。

             

          posted @ 2010-08-15 12:56 dennis 閱讀(3994) | 評論 (3)編輯 收藏

          Clojure 的并發(一) Ref和STM
          Clojure 的并發(二)Write Skew分析
          Clojure 的并發(三)Atom、緩存和性能
          Clojure 的并發(四)Agent深入分析和Actor
          Clojure 的并發(五)binding和let
          Clojure的并發(六)Agent可以改進的地方
          Clojure的并發(七)pmap、pvalues和pcalls
          Clojure的并發(八)future、promise和線程 

          八、future、promise和線程

          1、Clojure中使用future是啟動一個線程,并執行一系列的表達式,當執行完成的時候,線程會被回收:
          user=> (def myfuture (future (+ 1 2)))
          #
          'user/myfuture
          user=> @myfuture
          3

          future接受一個或者多個表達式,并將這些表達式交給一個線程去處理,上面的(+ 1 2)是在另一個線程計算的,返回的future對象可以通過deref或者@宏來阻塞獲取計算的結果。

          future函數返回的結果可以認為是一個類似java.util.concurrent.Future的對象,因此可以取消:
          user=> (future-cancelled? myfuture)
          false
          user
          => (future-cancel myfuture)
          false

          也可以通過謂詞future?來判斷一個變量是否是future對象:
          user=> (future? myfuture)
          true

          2、Future的實現,future其實是一個宏,它內部是調用future-call函數來執行的:
          (defmacro future
            [
          & body] `(future-call (fn [] ~@body)))
          可以看到,是將body包裝成一個匿名函數交給future-call執行,future-call接受一個Callable對象:

          (defn future-call 
            [
          ^Callable f]
            (let [fut (.submit clojure.lang.Agent
          /soloExecutor f)]
              (reify 
               clojure.lang.IDeref 
                (deref [_] (.get fut))
               java.util.concurrent.Future
                (get [_] (.get fut))
                (get [_ timeout unit] (.get fut timeout unit))
                (isCancelled [_] (.isCancelled fut))
                (isDone [_] (.isDone fut))
                (cancel [_ interrupt
          ?] (.cancel fut interrupt?)))))i

          將傳入的Callable對象f提交給Agent的soloExecuture
          final public static ExecutorService soloExecutor = Executors.newCachedThreadPool();

          執行,返回的future對象賦予fut,接下來是利用clojure 1.2引入的reify定義了一個匿名的數據類型,它有兩種protocol:clojure.lang.IDeref和java.utill.concurrent.Future。其中IDeref定義了deref方法,而Future則簡單地將一些方法委托給fut對象。protocol你可以理解成java中的接口,這里就是類似多態調用的作用。

          這里有個地方值的學習的是,clojure定義了一個future宏,而不是直接讓用戶使用future-call,這符合使用宏的規則:避免匿名函數。因為如果讓用戶使用future-call,用戶需要將表達式包裝成匿名對象傳入,而提供一個宏就方便許多。

          3、啟動線程的其他方法,在clojure中完全可以采用java的方式去啟動一個線程:
          user=> (.start (Thread. #(println "hello")))
          nil
          hello

          4、promise用于線程之間的協調通信,當一個promise的值還沒有設置的時候,你調用deref或者@想去解引用的時候將被阻塞:
          user=> (def mypromise (promise))
          #
          'user/mypromise
          user=> @mypromise

          在REPL執行上述代碼將導致REPL被掛起,這是因為mypromise還沒有值,你直接調用了@mypromise去解引用導致主線程阻塞。

          如果在調用@宏之前先給promise設置一個值的話就不會阻塞:
          user=> (def mypromise (promise))
          #
          'user/mypromise
          user=> (deliver mypromise 5)
          #
          <AFn$IDeref$db53459f@c0f1ec: 5>
          user
          => @mypromise               
          5

          通過調用deliver函數給mypromise傳遞了一個值,這使得后續的@mypromise直接返回傳遞的值5。顯然promise可以用于不同線程之間的通信和協調。

          5、promise的實現:promise的實現非常簡單,是基于CountDownLatch做的實現,內部除了關聯一個CountDownLatch還關聯一個atom用于存儲值:
          (defn promise
            []
            (let [d (java.util.concurrent.CountDownLatch. 
          1)
                  v (atom nil)]
              (reify 
               clojure.lang.IDeref
                (deref [_] (.await d) @v)
               clojure.lang.IFn
                (invoke [
          this x]
                  (locking d
                    (
          if (pos? (.getCount d))
                      (
          do (reset! v x)
                          (.countDown d)
                          
          this)
                      (
          throw (IllegalStateException. "Multiple deliver calls to a promise"))))))))

          d是一個CountDownLatch,v是一個atom,一開始值是nil。返回的promise對象也是通過reify定義的匿名數據類型,他也是有兩個protocol,一個是用于deref的IDeref,簡單地調用d.await()阻塞等待;另一個是匿名函數,接受兩個參數,第一個是promise對象自身,第二個參數是傳入的值x,當d的count還大于0的請看下,設置v的值為x,否則拋出異常的多次deliver了。查看下deliver函數,其實就是調用promise對象的匿名函數protocol:
          (defn deliver
            {:added 
          "1.1"}
            [promise val] (promise val))


          posted @ 2010-08-08 10:46 dennis 閱讀(5816) | 評論 (0)編輯 收藏

          Clojure 的并發(一) Ref和STM
          Clojure 的并發(二)Write Skew分析
          Clojure 的并發(三)Atom、緩存和性能
          Clojure 的并發(四)Agent深入分析和Actor
          Clojure 的并發(五)binding和let
          Clojure的并發(六)Agent可以改進的地方
          Clojure的并發(七)pmap、pvalues和pcalls
          Clojure的并發(八)future、promise和線程

          七、并發函數pmap、pvalues和pcalls

           1、pmap是map的進化版本,map將function依次作用于集合的每個元素,pmap也是這樣,但是它對于每個集合中的元素都是提交給一個線程去執行function,也就是并行地對集合里的元素執行指定的函數。通過一個例子來解釋下。我們先定義一個make-heavy函數用于延時執行某個函數:
          (defn make-heavy [f]
                  (fn [
          & args]
                      (Thread
          /sleep 1000)
                      (apply f args)))

          make-heavy接受一個函數f作為參數,返回一個新的函數,它延時一秒才實際執行f。我們利用make-heavy包裝inc,然后執行下map:

          user=> (time (doall (map (make-heavy inc) [1 2 3 4 5])))
          "Elapsed time: 5005.115601 msecs"
          (
          2 3 4 5 6)

          可以看到總共執行了5秒,這是因為map依次將包裝后的inc作用在每個元素上,每次調用都延時一秒,總共5個元素,因此延時了5秒左右。這里使用doall,是為了強制map返回的lazy-seq馬上執行。

          如果我們使用pmap替代map的話:
          user=> (time (doall (pmap (make-heavy inc) [1 2 3 4 5])))
          "Elapsed time: 1001.146444 msecs"
          (
          2 3 4 5 6)

          果然快了很多,只用了1秒多,顯然pmap并行地將make-heavy包裝后的inc作用在集合的5個元素上,總耗時就接近于于單個調用的耗時,也就是一秒。


          2、pvalues和pcalls是在pmap之上的封裝,pvalues是并行地執行多個表達式并返回執行結果組成的LazySeq,pcalls則是并行地調用多個無參數的函數并返回調用結果組成的LazySeq。

          user=> (pvalues (+ 1 2) (- 1 2) (* 1 2) (/ 1 2))
          (
          3 -1 2 1/2)

          user=> (pcalls #(println "hello") #(println "world"))
          hello
          world
          (nil nil)

          3、pmap的并行,從實現上來說,是集合有多少個元素就使用多少個線程:
           1 (defn pmap
           2   {:added "1.0"}
           3   ([f coll]
           4    (let [n (+ 2 (.. Runtime getRuntime availableProcessors))
           5          rets (map #(future (f %)) coll)
           6          step (fn step [[x & xs :as vs] fs]
           7                 (lazy-seq
           8                  (if-let [s (seq fs)]
           9                    (cons (deref x) (step xs (rest s)))
          10                    (map deref vs))))]
          11      (step rets (drop n rets))))
          12   ([f coll & colls]
          13    (let [step (fn step [cs]
          14                 (lazy-seq
          15                  (let [ss (map seq cs)]
          16                    (when (every? identity ss)
          17                      (cons (map first ss) (step (map rest ss)))))))]
          18      (pmap #(apply f %) (step (cons coll colls))))))

          在第5行,利用map和future將函數f作用在集合的每個元素上,future是將函數f(實現callable接口)提交給Agent的CachedThreadPool處理,跟agent的send-off共用線程池

          但是由于有chunked-sequence的存在,實際上調用的線程數不會超過chunked的大小,也就是32。事實上,pmap啟動多少個線程取決于集合的類型,對于chunked-sequence,是以32個元素為單位來批量執行,通過下面的測試可以看出來,range返回的是一個chunked-sequence,clojure 1.1引入了chunked-sequence,目前那些返回LazySeq的函數如map、filter、keep等都是返回chunked-sequence:

          user=> (time (doall (pmap (make-heavy inc) (range 0 32))))
          "Elapsed time: 1003.372366 msecs"
          (
          1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32)

          user
          => (time (doall (pmap (make-heavy inc) (range 0 64))))
          "Elapsed time: 2008.153617 msecs"
          (
          1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64)

          可以看到,對于32個元素,執行(make-heavy inc)耗費了一秒左右;對于64個元素,總耗時是2秒,這可以證明64個元素是分為兩個批次并行執行,一批32個元素,啟動32個線程(可以通過jstack查看)。


          并且pmap的執行是半延時的(semi-lazy),前面的總數-(cpus+2)個元素是一個一個deref(future通過deref來阻塞獲取結果),后cpus+2個元素則是一次性調用map執行deref。

          4、pmap的適用場景取決于將集合分解并提交給線程池并行執行的代價是否低于函數f執行的代價,如果函數f的執行代價很低,那么將集合分解并提交線程的代價可能超過了帶來的好處,pmap就不一定能帶來性能的提升。pmap只適合那些計算密集型的函數f,計算的耗時超過了協調的代價。

          5、關于chunked-sequence可以看看這篇報道,也可以參考Rich Hickey的PPT。chunk sequence的思路類似批量處理來提高系統的吞吐量。


          posted @ 2010-08-04 23:54 dennis 閱讀(3470) | 評論 (0)編輯 收藏

          Clojure 的并發(一) Ref和STM
          Clojure 的并發(二)Write Skew分析
          Clojure 的并發(三)Atom、緩存和性能
          Clojure 的并發(四)Agent深入分析和Actor
          Clojure 的并發(五)binding和let
          Clojure的并發(六)Agent可以改進的地方
          Clojure的并發(七)pmap、pvalues和pcalls
          Clojure的并發(八)future、promise和線程

              前面幾節基本介紹Clojure對并發的支持,估計這個系列還能寫個兩三節,介紹下一些常見的concurrent function的使用。談了很多優點,現在想談談clojure的一些缺憾,例如Agent系統。

              Agent在前面已經介紹過了,用于異步更新,基本的原理是將更新操作封裝為action,提交給線程池處理。內部有兩個線程池,固定大小(cpus+2)的線程池用于處理send發送的action,而send-off發送的action則是由一個Cached ThreadPool處理的。因此,如果你的更新操作很耗時或者會阻塞(如IO操作),那么通常是建議使用send-off,而send適合一些計算密集型的更新操作。兩個線程池的聲明如下(Agent.java):
          1 final public static ExecutorService pooledExecutor =
          2         Executors.newFixedThreadPool(2 + Runtime.getRuntime().availableProcessors());
          3 
          4 final public static ExecutorService soloExecutor = Executors.newCachedThreadPool();

              說說我認為Agent可以改進的地方。
              首先是從實踐角度出發,通常我們要求new一個線程池的時候,要設置線程池內線程的name,以方便查看和調試。因此Clojure這里簡單的new線程池是一個可以改進的地方,應當自定義一個ThreadFactory,給clojure的Agent線程提供明確的名稱。

              其次,由于這兩個線程池是全局的,因此clojure提供了shutdown-agents的方法用于關閉線程池。但是由于這些線程池內的線程并非daemon,因此如果你沒有明確地調用shutdown-agents,jvm也可以正常退出。我們都知道,如果還有dadmon線程沒有終止,JVM是無法退出的。如果JVM只剩下daemon線程,那么jvm就會自動退出。從實踐角度,應當明確地要求用戶調用shutdown-agents來關閉Agent系統,妥善終止線程,并且Agent的線程池應當延遲初始化,只在必要的時候創建,而非現在的靜態變量。所以,在實現ThreadFactory的時候,應當設置生成的線程為daemon。

              第三,同樣由于線程池是全局的,關閉了卻沒有辦法重新啟動,這不能不說是一個缺憾。Clojure沒有提供重新啟動的方法。

              第四,線程池簡單地分為兩類,從理論上足以滿足大部分應用的要求。但是在real world的應用上,我們通常不敢用CachedThreadPool,這是為了防止內存不受控,導致線程創建過多直接OOM。通常我們會使用固定大小的線程池,但是clojure固定大小的線程池只有一個,并且大小寫死為cpus+2,這就沒有了控制的余地。我還是希望clojure能提供允許自定義Agent線程池的方法,可以在創建的時候傳入線程池,如:
          (agent :executor (java.util.concurrent.Executors/newFixedThreadPool 2))

          或者提供新的API,如set-executor!來設置agent使用的線程池,如果沒有自定義線程池再使用全局的。當然也需要提供一個關閉agent自定義線程池的API:
          (shutdown-agent agent)

              需要自定義線程池是另一個原因是為了最大化地發揮線程池的效率,我們知道,線程池只有在執行“同構”任務的時候才能發揮最大的效率,如果有的 action快,有的action慢,那么該快的快不起來,慢的卻擠占了快的action的執行時間。通過給Agent設定自己的線程池某些程度上可以解決這個問題。


             Agent的整個模型是很優雅的,但是確實還有這些地方不是特別讓人滿意,希望以后會得到改進。

          posted @ 2010-07-30 21:35 dennis 閱讀(3252) | 評論 (0)編輯 收藏



              今年我被分配了不少參與面試應聘者的任務,大多數是做電話面試,偶爾做F2F的面試。這是我第一次這么頻繁地面試別人,過去我都是被別人面試。主要想談談一些感想,從面試官的角度給面試者一些建議。因為做面試官的經驗就這么多,我大概地講,有興趣的大概地看。

              面試者需要首先知道自己是想應聘什么職位,很多人的簡歷到我這里來,都不知道自己是面試什么職位,那么聊的時候就經常對不上號,原來你不是我們想要那種人,我不是去應聘你這個職位。我想在投簡歷的時候需要明確下自己想來這家公司做什么,這樣可以更明確一些,節省大家時間。并且面試者準備起來也能更充分一些。

              做電話面試的時候,我比較怕那種聲音很小的朋友,聽起來費勁,我問起來費勁。我提問的方式,一般是喜歡窮追到底,根據對方的描述即興發問,一方面查看知識掌握得牢固不牢固,一方面查看反應能力怎么樣。如果你聲音很小,我發問的頻率降低了,我對面試者的評價也自然降低了。

              另外,做電話面試,面試者最好能在接電話的時候找個安靜的地方,背景的嘈雜也會影響面試的效果,有時候不得不我去提醒對方找一個安靜一些地方。您老做其他公司的電話面試,還是別當著同事老板的面嘛。

              公司還有做視頻面試,視頻面試的時候一般是通過旺旺,做語音聊天,這時候面試者還是需要注意下個人的舉止。面對面的時候,大家會小心注意一些,但是做視頻面試的時候,由于你面對只是一個攝像頭,沒有那么緊張,一些舉止可能不恰當。有一次我視頻面試一個同學,面試聊著聊著,他都將腳抬到桌子上,對著攝像頭了,盡管我并不介意,但是遇上其他面試官就不一定那么好說話了。這位同學最后沒過,不過不是我這關刷下的了。

              我看簡歷的時候,一般都是先看項目經歷,基本上你做過什么項目,大體能告訴我你擅長的技術方向,接下來看個人簡介,與你的項目經歷是否符合,如果你描述了你擅長什么,精通什么,那么后續的面試我就問這些東西。當然,如果你的技術方向跟我們的要求完全不吻合,那我可能只是打個電話走過場。
              簡歷上能讓人眼前一亮的地方包括:除了Java之外,你還會python、Ruby甚至Scheme之類的其他語言;你參與了某些開源項目;你對某個領域的技術特別擅長,并且做過這方面的學習研究;你做過性能優化,系統調優之類的功能;你描述了對某個系統做出的一些優化或者改進;你有自己的技術博客;你讀過某些開源項目的源碼;你指導過別人的工作……
              簡歷上讓人疲倦的地方:一堆同類項目的羅列,沒有鮮明的描述,沒有說明你在項目里做了什么;只有項目羅列,沒有個人描述;只有結果,沒有過程;只有一堆名詞,沒有特別擅長的亮點。

              這大半年來,也面試了不少牛人,但是最后能進來的卻是寥寥無幾,我覺的特別牛的,可能老大們覺的某個方面不合適,也可能待遇上談不攏,最終都來不了,很可惜。我也提議過改進面試的方式,不僅僅是聊天式的面試,最好能增加一個上機面試,給一道很簡單的題目,你做出來看看,不僅僅看程序寫的對不對,更多看看你寫的代碼是否符合工程規范,是否有單元測試,變量命名是否合理,在思路上有沒有亮點等等。我總覺的簡單地通過面談來考察一個人是很難靠譜的,程序員還是要以代碼說話。后來也寫了個上機面試的提綱,不過最終這件事情也是不了了之。

             另外,面試通過其實應該是萬里長征的第一步,公司在招進一個人之后,基本上是不管不問了,這也可以理解,畢竟都是成年人了;但是對于外地來的員工,很多都是獨身一人就跑到杭州來的,情感上適應是第一步,還有找房子、熟悉周邊環境等等瑣事。既然在前期招聘已經花了很大力氣,如果可以在員工入職后稍微關心下新入員工的狀況,這是特別劃算的買賣。入職剛轉正的員工馬上走人,這對公司的損失更大。

          posted @ 2010-07-25 23:00 dennis 閱讀(2062) | 評論 (0)編輯 收藏


              轉眼間快到8月,已經過去了兩個季度,是時候稍微總結下干了什么,以后想干什么。從春節到現在,我仍然是做淘寶的消息中間件Notify的開發,中間額外去做了一個多月的另一個項目的開發,重拾了web開發的一些東西。
             
              這半年來Notify的改進集中在通訊層的改造,引入AMQP的訂閱模型,以及將重要消息從oracle遷移到mysql做HA方案,這一過程是一個慢慢穩定系統的過程。新版本剛出來的時候有不少BUG,有些BUG很嚴重,幸運的是沒有造成嚴重的后果,再一次提醒我小心,小心,再小心;小心是一方面,工作有沒有做到位是另一個方面,暴露出來的問題還是單元測試不全面,以及麻痹大意心態下的不警惕,對關鍵代碼認識不清,code review也沒有做到位等等。

              Notify做到現在,剩下的問題是一些很難解決,又非常關鍵性的問題,如消息的去重,消息的順序性,以及消息的可靠存儲。我說的這些問題都是在分布式集群環境下需要解決的問題,如果僅僅是單臺服務器,這些問題的解決還算不上特別困難。消息的去重,基本的思路是在客戶端和服務器之間各自維持一個狀態,用于保存當前消息的處理情況,依據這個情況來做消息的去重,但是狀態的保存對服務器和客戶端來說都是一個額外的負擔,并且很難做到可靠的存儲,如果狀態丟了,去重的目的就沒辦法做到。ActiveMQ里是在服務器和客戶端都維持了一個bitmap做重復的檢測,但是這個bitmap大小必然是受限的,并且無法持久保存的。消息的在集群環境下的順序性,涉及到集群環境下的事件的時間順序問題,除了使用分布式鎖來保證一致性之外,暫時也沒有好的思路去解決。消息的可靠存儲,今年我們的目標至少是脫離oracle,目前實現的HA mysql雙寫的實現已經開始部署到交易這樣的核心系統上,第三個季度將慢慢地全部切換過去。下一步的目標是將消息存儲到key-value系統上,但是需要解決的是索引的問題,因為消息的恢復涉及到查詢,并且需要根據一些特殊條件做查詢以應付諸如盡快恢復重要消息這樣的功能,因此目前的一些key-value系統要么在索引功能上太弱,要么在集群功能上太弱,要么在大數據量存儲上有局限,因此不是特別切合我們的場景,因此一個可行的方案是將消息的header繼續存儲在關系數據庫,方便做查詢,而將數據較大的body存儲在key-value上,減輕數據庫的負擔。今年,我們還是希望能在以上3個方向某個方向做出突破。

             這半年來技術上的收獲,第一個季度業余時間都去打游戲了,沒方什么心思在學習和工作上,后來去學習了下ASM,總算對java的byte code,以及jvm執行byte code過程有了個理解,然后利用ASM去搞了aviator這個項目。接下來開始做服務器的SEDA改造,這個過程完成了部分,但是不滿意,SEDA的模型過于理論化,模型是好的,但是在stage controller的實現上目前沒有可供參考的經驗,做到資源的自動控制更需要實際的測試和實踐,基本的指導原則只能作為參考。另外,最近下決心去重構整個項目,從一個一個類看起,看到不爽的地方馬上去做重構,這個過程,我又去重看了下《重構》中的原則,在談論諸如分布式、海量數據存儲、云計算這樣的大詞之前,我需要的做的仍然是將代碼寫好,寫的漂亮。也許是時候回到本源,再去重讀下《設計模式》,重讀下《重構》,既然我還在寫java代碼,那還是希望寫的更好點。
             另外,我現在喜歡上了clojure語言,并且正兒八經地找了本書好好學習,從源碼和bytecode入手去理解它的實現。我為什么喜歡clojure?
             首先,它是LISP的方言之一,LISP的優點它全有,LISP的缺點它能想辦法避免。Clojure也有宏,也有quote,也有將procedure as data的能力,Clojure的數據結構都是immutable,此外還是persistence,避免了immutable數據大量拷貝帶來的開銷。Clojure的數據結構還天生是lazy的,表達能力上一個臺階。Clojure在語法上更多變化,某些程度上降低了括號的使用頻率,這一點有利有弊。Clojure的內在原則是一致的,核心語法非常簡單,它沒有多種范式可供選擇,因此沒有scala那樣復雜的類型系統,沒有為了包容java程序員引入的OO模型(有替代品),使用clojure最好的方式是函數式地,但是它的擴展能力允許你去嘗試各種范式。

             其次,Clojure對并發的支持更符合一般程序員的理解,它只是將鎖替換成了事務,利用STM去保存可變狀態,但是卻避免鎖帶來的缺點——死鎖、活鎖、競爭條件。它沒有引入新的模型,這對習慣于用鎖編程的同學來說,STM沒有很大差異,你可以將它理解成內存型數據庫。

             第三,最重要的一點,Clojure是實現于JVM之上的,Java上的任何東西它都能直接利用,并且利用type hint之類的手段可以做到性能上沒有損失。盡管Java語言有千般不是,但是寄生于整個平臺之上的開源生態系統是任何其他社區都很難比擬的,放棄Java平臺這個寶庫,暫時還做不到。過去學習scheme,學習common lisp,更多的目的是開闊眼界,現在能實際地使用,還能有比這更幸福的事情嗎?

             下半年技術上想學習什么呢?除了clojure之外,我想去看下haskell,了解下什么是mond,除此之外,就是收收心將《算法導論》讀完吧。另外,收起對awk和shell編程的偏見,好好熟悉下這兩個工具,dirty and quickly的干活有時候還是很重要的。

              我還是個典型的碼農,喜歡寫代碼,喜歡嘗試新東西,至少熱情和好奇心還殘存一些,那么就繼續當好碼農吧。
           

          posted @ 2010-07-24 20:51 dennis 閱讀(2012) | 評論 (1)編輯 收藏

          Clojure 的并發(一) Ref和STM
          Clojure 的并發(二)Write Skew分析
          Clojure 的并發(三)Atom、緩存和性能
          Clojure 的并發(四)Agent深入分析和Actor
          Clojure 的并發(五)binding和let
          Clojure的并發(六)Agent可以改進的地方
          Clojure的并發(七)pmap、pvalues和pcalls
          Clojure的并發(八)future、promise和線程

          五、binding和let

              前面幾節已經介紹了Ref、Atom和Agent,其中Ref用于同步協調多個狀態變量,Atom只能用于同步獨立的狀態變量,而Agent則是允許異步的狀態更新。這里將介紹下binding,用于線程內的狀態的管理。

          1、binding和let:
          當你使用def定義一個var,并傳遞一個初始值給它,這個初始值就稱為這個var的root binding。這個root binding可以被所有線程共享,例如:
          user=> (def foo 1)
          #
          'user/foo
              那么對于變量foo來說,1是它的root binding,這個值對于所有線程可見,REPL的主線程可見:
          user=> foo
          1
             啟動一個獨立線程查看下foo的值:
          user=> (.start (Thread. #(println foo)))
          nil
           
          1
            可以看到,1這個值對于所有線程都是可見的。
           
            但是,利用binding宏可以給var創建一個thread-local級別的binding:
          (binding [bindings] & body)

            binding的范圍是動態的,binding只對于持有它的線程是可見的,直到線程執行超過binding的范圍為止,binding對于其他線程是不可見的。
          user=> (binding [foo 2] foo)
          2

            粗看起來,binding和let非常相似,兩者的調用方式近乎一致:
          user=> (let [foo 2] foo)
          2

            從一個例子可以看出兩者的不同,定義一個print-foo函數,用于打印foo變量:
          user=> (defn print-foo [] (println foo))
          #
          'user/print-foo

            foo不是從參數傳入的,而是直接從當前context尋找的,因此foo需要預先定義。分別通過let和binding來調用print-foo:
          user=> (let [foo 2] (print-foo))
          1
          nil

            可以看到,print-foo仍然打印的是初始值1,而不是let綁定的2。如果用binding:
          user=> (binding [foo 2] (print-foo))
          2
          nil

             print-foo這時候打印的就是binding綁定的2。這是為什么呢?這是由于let的綁定是靜態的,它并不是改變變量foo的值,而是用一個詞法作用域的foo“遮蔽”了外部的foo的值。但是print-foo卻是查找變量foo的值,因此let的綁定對它來說是沒有意義的,嘗試利用set!去修改let的foo:
          user=> (let [foo 2] (set! foo 3))
          java.lang.IllegalArgumentException: Invalid assignment target (NO_SOURCE_FILE:
          12)
            
             Clojure告訴你,let中的foo不是一個有效的賦值目標,foo是不可變的值。set!可以修改binding的變量:
          user=> (binding [foo 2] (set! foo 3) (print-foo))
          3
          nil

          2、Binding的妙用:


          Binding可以用于實現類似AOP編程這樣的效果,例如我們有個fib函數用于計算階乘:
          user=> (defn fib [n]
                   (loop [ n n r 
          1]
                      (
          if (= n 1)
                          r
                          (recur (dec n) (
          * n r)))))

          然后有個call-fibs函數調用fib函數計算兩個數的階乘之和:
          user=> (defn call-fibs [a b]
                    (
          + (fib a) (fib b)))
          #
          'user/call-fibs
          user=> (call-fibs 3 3)
          12

            現在我們有這么個需求,希望使用memoize來加速fib函數,我們不希望修改fib函數,因為這個函數可能其他地方用到,其他地方不需要加速,而我們希望僅僅在調用call-fibs的時候加速下fib的執行,這時候可以利用binding來動態綁定新的fib函數:
          user=> (binding [fib (memoize fib)] 
                          (call
          -fibs 9 10))
          3991680

             在沒有改變fib定義的情況下,只是執行call-fibs的時候動態改變了原fib函數的行為,這不是跟AOP很相似嗎?

             但是這樣做已經讓call-fibs這個函數不再是一個“純函數”,所謂“純函數”是指一個函數對于相同的參數輸入永遠返回相同的結果,但是由于binding可以動態隱式地改變函數的行為,導致相同的參數可能返回不同的結果,例如這里可以將fib綁定為一個返回平方值的函數,那么call-fibs對于相同的參數輸入產生的值就改變了,取決于當前的context,這其實是引入了副作用。因此對于binding的這種使用方式要相當慎重。這其實有點類似Ruby中的open class做monkey patch,你可以隨時隨地地改變對象的行為,但是你要承擔相應的后果。

          3、binding和let的實現上的區別


          前面已經提到,let其實是詞法作用域的對變量的“遮蔽”,它并非重新綁定變量值,而binding則是在變量的root binding之外在線程的ThreadLocal內存儲了一個綁定值,變量值的查找順序是先查看ThreadLocal有沒有值,有的話優先返回,沒有則返回root binding。下面將從Clojure源碼角度分析。

          變量在clojure是存儲為Var對象,它的內部包括:

          //這是變量的ThreadLocal值存儲的地方
          static ThreadLocal<Frame> dvals = new ThreadLocal<Frame>(){

              
          protected Frame initialValue(){
                  
          return new Frame();
              }
          };

          volatile Object root;  //這是root binding
          public final Symbol sym;   //變量的符號
          public final Namespace ns;  //變量的namespace

          通過def定義一個變量,相當于生成一個Var對象,并將root設置為初始值。

          先看下let表達式生成的字節碼:
          (let [foo 3] foo)
          字節碼:
          public class user$eval__4349 extends clojure/lang/AFunction  {

            
          // compiled from: NO_SOURCE_FILE
            
          // debug info: SMAP
          eval__4349.java
          Clojure
          *S Clojure
          *F
          + 1 NO_SOURCE_FILE
          NO_SOURCE_PATH
          *L
          0#1,1:0
          *E

            
          // access flags 25
            public final static Ljava/lang/Object; const__0

            
          // access flags 9
            public static <clinit>()V
             L0
              LINENUMBER 
          2 L0
              ICONST_3
              INVOKESTATIC java
          /lang/Integer.valueOf (I)Ljava/lang/Integer;
              PUTSTATIC user$eval__4349.const__0 : Ljava
          /lang/Object;

              RETURN
              MAXSTACK 
          = 0
              MAXLOCALS 
          = 0

            
          // access flags 1
            public <init>()V
             L0
              LINENUMBER 
          2 L0
             L1
              ALOAD 
          0
              INVOKESPECIAL clojure
          /lang/AFunction.<init> ()V
             L2
              RETURN
              MAXSTACK 
          = 0
              MAXLOCALS 
          = 0

            
          // access flags 1
            public invoke()Ljava/lang/Object; throws java/lang/Exception 
             L0
              LINENUMBER 
          2 L0
              GETSTATIC user$eval__4349.const__0 : Ljava
          /lang/Object;
              ASTORE 
          1
             L1
              ALOAD 
          1
             L2
              LOCALVARIABLE foo Ljava
          /lang/Object; L1 L2 1
             L3
              LOCALVARIABLE 
          this Ljava/lang/Object; L0 L3 0
              ARETURN
              MAXSTACK 
          = 0
              MAXLOCALS 
          = 0
          }

              可以看到foo并沒有形成一個Var對象,而僅僅是將3存儲為靜態變量,最后返回foo的時候,也只是取出靜態變量,直接返回,沒有涉及到變量的查找。let在編譯的時候,將binding作為編譯的context靜態地編譯body的字節碼,body中用到的foo編譯的時候就確定了,沒有任何動態性可言。

              再看同樣的表達式替換成binding宏,因為binding只能重新綁定已有的變量,所以需要先定義foo:
          user=> (def foo 100)
          #
          'user/foo
          user=> (binding [foo 3] foo)

              binding是一個宏,展開之后等價于:
          (let []
                   (push
          -thread-bindings (hash-map (var foo) 3))
                   (
          try
                      foo
                   (
          finally
                      (pop
          -thread-bindings))))

              首先是將binding的綁定列表轉化為一個hash-map,其中key為變量foo,值為3。函數push-thread-bindings:

          (defn push-thread-bindings
               [bindings]
               (clojure.lang.Var
          /pushThreadBindings bindings))
             
              其實是調用Var.pushThreadBindings這個靜態方法:
          public static void pushThreadBindings(Associative bindings){
              Frame f 
          = dvals.get();
              Associative bmap 
          = f.bindings;
              
          for(ISeq bs = bindings.seq(); bs != null; bs = bs.next())
                  {
                  IMapEntry e 
          = (IMapEntry) bs.first();
                  Var v 
          = (Var) e.key();
                  v.validate(v.getValidator(), e.val());
                  v.count.incrementAndGet();
                  bmap 
          = bmap.assoc(v, new Box(e.val()));
                  }
              dvals.set(new Frame(bindings, bmap, f));
          }

              pushThreadBindings是將綁定關系放入一個新的frame(新的context),并存入ThreadLocal變量dvals。pop-thread-bindings函數相反,彈出一個Frame,它實際調用的是Var.popThreadBindings靜態方法:
          public static void popThreadBindings(){
              Frame f 
          = dvals.get();
              
          if(f.prev == null)
                  
          throw new IllegalStateException("Pop without matching push");
              
          for(ISeq bs = RT.keys(f.frameBindings); bs != null; bs = bs.next())
                  {
                  Var v 
          = (Var) bs.first();
                  v.count.decrementAndGet();
                  }
              dvals.set(f.prev);
          }

             在執行宏的body表達式,也就是取foo值的時候,實際調用的是Var.deref靜態方法取變量值:
          final public Object deref(){
              
          //先從ThreadLocal找
              Box b = getThreadBinding();
              
          if(b != null)
                  
          return b.val;
              
          //如果有定義初始值,返回root binding
              if(hasRoot())
                  
          return root;
              
          throw new IllegalStateException(String.format("Var %s/%s is unbound.", ns, sym));
          }

              看到是先嘗試從ThreadLocal找:
          final Box getThreadBinding(){
              
          if(count.get() > 0)
                  {
                  IMapEntry e 
          = dvals.get().bindings.entryAt(this);
                  
          if(e != null)
                      
          return (Box) e.val();
                  }
              
          return null;
          }

             找不到,如果有初始值就返回初始的root binding,否則拋出異常:Var user/foo is unbound.
             binding表達式最后生成的字節碼,做的就是上面描述的這些函數調用,有興趣地可以自行分析。

             

          posted @ 2010-07-23 23:19 dennis 閱讀(4993) | 評論 (1)編輯 收藏

             
              本人的西大學歷證書,如果你也想要一本,請到http://www.51by.com/wenping/diploma.php 自動生成。





          posted @ 2010-07-20 09:12 dennis 閱讀(1308) | 評論 (0)編輯 收藏

          僅列出標題
          共56頁: First 上一頁 6 7 8 9 10 11 12 13 14 下一頁 Last 
          主站蜘蛛池模板: 枣阳市| 施秉县| 曲阳县| 衢州市| 醴陵市| 浮山县| 大荔县| 北川| 泸西县| 尚志市| 信宜市| 陕西省| 潞城市| 抚顺县| 遂平县| 乐昌市| 洛阳市| 天长市| 永城市| 郓城县| 湖南省| 怀安县| 邵阳县| 延寿县| 环江| 平塘县| 鄄城县| 唐海县| 洛阳市| 阿克陶县| 驻马店市| 康乐县| 方正县| 四会市| 惠水县| 额敏县| 吉林省| 淳安县| 南安市| 东乡县| 平果县|