庄周梦蝶

生活、程序、未来
   :: 首页 ::  ::  :: 聚合  :: 管理


    这个问题的由来是有一个朋友报告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 阅读(4884) | 评论 (0)编辑 收藏

非终稿,有兴趣先看看。

posted @ 2010-08-17 18:07 dennis 阅读(4114) | 评论 (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 阅读(3985) | 评论 (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 阅读(5796) | 评论 (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 阅读(3460) | 评论 (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 阅读(3241) | 评论 (0)编辑 收藏



    今年我被分配了不少参与面试应聘者的任务,大多数是做电话面试,偶尔做F2F的面试。这是我第一次这么频繁地面试别人,过去我都是被别人面试。主要想谈谈一些感想,从面试官的角度给面试者一些建议。因为做面试官的经验就这么多,我大概地讲,有兴趣的大概地看。

    面试者需要首先知道自己是想应聘什么职位,很多人的简历到我这里来,都不知道自己是面试什么职位,那么聊的时候就经常对不上号,原来你不是我们想要那种人,我不是去应聘你这个职位。我想在投简历的时候需要明确下自己想来这家公司做什么,这样可以更明确一些,节省大家时间。并且面试者准备起来也能更充分一些。

    做电话面试的时候,我比较怕那种声音很小的朋友,听起来费劲,我问起来费劲。我提问的方式,一般是喜欢穷追到底,根据对方的描述即兴发问,一方面查看知识掌握得牢固不牢固,一方面查看反应能力怎么样。如果你声音很小,我发问的频率降低了,我对面试者的评价也自然降低了。

    另外,做电话面试,面试者最好能在接电话的时候找个安静的地方,背景的嘈杂也会影响面试的效果,有时候不得不我去提醒对方找一个安静一些地方。您老做其他公司的电话面试,还是别当着同事老板的面嘛。

    公司还有做视频面试,视频面试的时候一般是通过旺旺,做语音聊天,这时候面试者还是需要注意下个人的举止。面对面的时候,大家会小心注意一些,但是做视频面试的时候,由于你面对只是一个摄像头,没有那么紧张,一些举止可能不恰当。有一次我视频面试一个同学,面试聊着聊着,他都将脚抬到桌子上,对着摄像头了,尽管我并不介意,但是遇上其他面试官就不一定那么好说话了。这位同学最后没过,不过不是我这关刷下的了。

    我看简历的时候,一般都是先看项目经历,基本上你做过什么项目,大体能告诉我你擅长的技术方向,接下来看个人简介,与你的项目经历是否符合,如果你描述了你擅长什么,精通什么,那么后续的面试我就问这些东西。当然,如果你的技术方向跟我们的要求完全不吻合,那我可能只是打个电话走过场。
    简历上能让人眼前一亮的地方包括:除了Java之外,你还会python、Ruby甚至Scheme之类的其他语言;你参与了某些开源项目;你对某个领域的技术特别擅长,并且做过这方面的学习研究;你做过性能优化,系统调优之类的功能;你描述了对某个系统做出的一些优化或者改进;你有自己的技术博客;你读过某些开源项目的源码;你指导过别人的工作……
    简历上让人疲倦的地方:一堆同类项目的罗列,没有鲜明的描述,没有说明你在项目里做了什么;只有项目罗列,没有个人描述;只有结果,没有过程;只有一堆名词,没有特别擅长的亮点。

    这大半年来,也面试了不少牛人,但是最后能进来的却是寥寥无几,我觉的特别牛的,可能老大们觉的某个方面不合适,也可能待遇上谈不拢,最终都来不了,很可惜。我也提议过改进面试的方式,不仅仅是聊天式的面试,最好能增加一个上机面试,给一道很简单的题目,你做出来看看,不仅仅看程序写的对不对,更多看看你写的代码是否符合工程规范,是否有单元测试,变量命名是否合理,在思路上有没有亮点等等。我总觉的简单地通过面谈来考察一个人是很难靠谱的,程序员还是要以代码说话。后来也写了个上机面试的提纲,不过最终这件事情也是不了了之。

   另外,面试通过其实应该是万里长征的第一步,公司在招进一个人之后,基本上是不管不问了,这也可以理解,毕竟都是成年人了;但是对于外地来的员工,很多都是独身一人就跑到杭州来的,情感上适应是第一步,还有找房子、熟悉周边环境等等琐事。既然在前期招聘已经花了很大力气,如果可以在员工入职后稍微关心下新入员工的状况,这是特别划算的买卖。入职刚转正的员工马上走人,这对公司的损失更大。

posted @ 2010-07-25 23:00 dennis 阅读(2052) | 评论 (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 阅读(2000) | 评论 (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 阅读(4972) | 评论 (1)编辑 收藏

   
    本人的西大学历证书,如果你也想要一本,请到http://www.51by.com/wenping/diploma.php 自动生成。





posted @ 2010-07-20 09:12 dennis 阅读(1304) | 评论 (0)编辑 收藏

仅列出标题
共56页: First 上一页 6 7 8 9 10 11 12 13 14 下一页 Last