-
-
Save JeffreyZhao/8644613 to your computer and use it in GitHub Desktop.
// 其实这题可以写的很简单,它的关键根本不是CAS操作。 | |
// 使用一个标志(flag也好,status判断也罢)来避免Start和Dispose重入是很容易 | |
// 想到的手段,很多同学也这么做了。大部分同学遇到的问题是:假如StartCore已经 | |
// 启动,那么如何让Dispose方法等待其完成。有些同学用while (true),有些同学用 | |
// 锁,但都避免不了让Dispose线程等待Start线程。 | |
// 这里“避免等待”的关键在于要跳出思维界限:谁说DisposeCore方法一定要在Dispose | |
// 方法里执行的?我们就不能在Start里销毁对象吗? | |
public class FineLockCalculator : CalculatorBase { | |
private enum Status { | |
NotStarted, | |
Starting, | |
Started, | |
Disposed | |
} | |
private readonly object _gate = new object(); | |
private Status _status = Status.NotStarted; | |
public virtual void Start() { | |
lock (_gate) { | |
if (_status != Status.NotStarted) | |
return; | |
_status = Status.Starting; | |
} | |
StartCore(); | |
lock (_gate) { | |
if (_status == Status.Starting) { | |
_status = Status.Started; | |
return; | |
} | |
} | |
DisposeCore(); | |
} | |
public void Dispose() { | |
lock (_gate) { | |
var origStatus = _status; | |
_status = Status.Disposed; | |
if (origStatus != Status.Started) | |
return; | |
} | |
DisposeCore(); | |
} | |
} | |
// 在Dispose方法中,我们对_status进行标记,但只在确定Started的情况下才 | |
// 调用DisposeCore。同理,在Start方法中假如发现_status已经变成了Disposed, | |
// 则原地调用DisposeCore销毁对象。使用这种方法,Dispose方法完全不需要做 | |
// 任何等待,整体等待时间极短。 | |
// 清晰起见,上述代码没有使用任何CAS操作,只是对于_status读写加以简单保护。 | |
// 假如需要的话,我们也可以把三个lock轻松修改为CAS操作,但这只是起到锦上 | |
// 添花的作用。 | |
// 有些同学一上来就搞CAS,然后发现问题之后只是简单的修补,于是代码越变越 | |
// 复杂。理清思路,划分好边界,处理好边界上的竞争关系,其实逻辑说到底就是 | |
// 这么简单. |
晕,那假如是在Start里开始监听,Dispose里关闭,你这...
这样做看起来没什么问题。学到一招。
受教了,只想到多线程在lock那儿的等待耗时,没注意到Dispose线程等待Start线程的问题,考虑问题还是不够全面。。。
嗯,老赵的状态转换和我的是类似的,不过省了一个Disposing
,Dispose
代码也省了不少,易读了不少。
顺便贴一下自己的代码,哈哈哈哈 https://gist.github.com/JeffreyZhao/8630308/#comment-993947
不过我是比较好奇这个类具体的计算函数(我猜会有个Calculate函数吧?)是怎么用的,也就是客户代码是怎么个乱序调Start
和Dispose
的。
没有Calculate
函数。StartCore
里面是开始监听一个数据源,并把计算结果表达出来。一个Calculator
对象可以被多个线程持有,在某一个阶段需要开启,但内部显然只能调用StartCore
一次。至于Dispose
,的确只有一个线程会调用,但在.NET里IDisposable
接口本身的协议是要求可以多次调用相当于调用一次。换个高大上的说法,是“幂等”的,Start
也是。
貌似没发现bug,不爽
public class BigLockCalculator extends CalculatorBase {
private interface Status {
int NotStarted =1;
int Starting =2;
int Started =3;
int Disposed =4;
}
private AtomicInteger _status = new AtomicInteger(Status.NotStarted);
private AtomicInteger _disposingFlag = new AtomicInteger(0);
private AtomicInteger _startingFlag = new AtomicInteger(0);
public override void Start() {
if(_status.get() != Status.NotStarted ) {
return;
}
if( _startingFlag.compareAndSwap(0,1)) {
try {
if(_status.compareAndSwap(Status.NotStarted, Status.Starting)) {
StartCore();
if(!_status.compareAndSwap(Status.Starting, Status.Started)) {
if(_status.get() == Status.Disposed ) {
DisposeCore();
return;
}
}
}
}finally {
_startingFlag.compareAndSwap(1,0);
}
}
}
public override void Dispose() {
if(_status.get() == Status.Disposed) {
return;
}
if( _disposingFlag.compareAndSwap(0,1)) {
try {
while(true) {
int currentStatus =_status.get();
if(currentStatus == Status.Disposed) {
return;
}
if(currentStatus == Status.NotStarted && _status.compareAndSwap(currentStatus, Status.Disposed )) {
return;
}else if(currentStatus == Status.Starting && _status.compareAndSwap(currentStatus, Status.Disposed )){
return;
}else if(currentStatus == Status.Started && _status.compareAndSwap(currentStatus, Status.Disposed )){
DisposeCore();
}
}
}finally {
_disposingFlag.compareAndSwap(1,0);
}
}
}
}
@JeffreyZhao 帮忙review下纯CAS版本,无锁。
@JeffreyZhao 弱弱的问一句,在Start()的最后一句为什么是DisposeCore()而不是Dispose()呢?在这里直接调用DisposeCore()是不是意味着,有个前提的假设是基类里实现的DisposeCore()可以被多次重复调用呢?
@dawnbreaks: 没仔细看,但为什么会那么复杂,按照我的代码改写成CAS的,应该十分简单才对。
@ivenxu: 当然应该调用DisposeCore
,再仔细看一下代码,DisposeCore
不会被重复调用。
Nice approach. The key point here is that thinking outside of box, let DisposeCore being called from Start().
@JeffreyZhao, 仔细看了,DisposeCore()确实不大可能被重复调用.再弱弱的多问一句,DisposeCore()在Start()中在什么样的场景才会被调用?我实在想不出来,因为在我看来在Start()里面执行完_status = Status.Started;就return;了。
@ivenxu Start()里面的DisposeCore()还是有可能会被调用的,比如正在执行StartCore()的时候调用了Dispose(),那么_status = Status.Disposed,然后StartCore()执行完毕后会调用DisposeCore(). 以上只是个人粗浅的理解。
又学了一手,太机智了。
不过仔细想想,这样对于调用者来说太trick了吧。比如我发现dispose结束了,心想太好了可以拔电源/拔网线了,但其实DisposeCore可能还没开始呢。
Start里还要调用DisposeCore?
你也没说清楚啊...Calculator执行完StartCore就完事了,你只说Start是启动计算器,又没有说计算的动作在StartCore里,以为像是个服务一样,启动了再执行其他的操作。。。