日期:2014-05-20  浏览次数:20823 次

Concurrent Synchronizer Framework(3)

?

这章来自3.3Queues》。

?

???????? 这个framework的核心是排队阻塞线程。这里的排队限制为先进先出,因此,framework不支持基于优先级的排队。

???????? 对于同步队列使用非阻塞队列,从而不用构造一些low-level的锁这一个选择,几乎是没有争论的。有两个选择,

CLH LockMCS Lock。原始的CLH Lock仅仅使用自旋锁,但是相对于MSC Lock,它更容易处理canceltimeout,所以选择了CLH Lock。最后设计出来的变种CLH Lock和原始的CLH Lock有较大的差别,需要描述一下。

?

???????? 一个CLH Queue看起来不太像一个queue,因为它的出队和入队操作像是一个锁(原文:because its enqueuing and dequeuing operations are intimately tied to its uses as a lock)。它是一个实用两个原子更新字段的linked queue。分别是headtail,在初始化时指向同一个node

?

???????? 一个新的node入队时是一个原子操作:

???????? do { pred = tail;} while(!tail.compareAndSet(pred, node));

?

???????? 每个noderelease 状态保存在它的前驱node中,所以它的自旋锁的自旋操作:

???????? while (pred.status != RELEASED) ; // spin

????????

出队操作在自旋后,要把head设置为node

head = node;

?

CLH Lock的优点:出队和入队都非常快;lock-free;无障碍(obstruction free,即使是在竞争情况下,总有一个线程赢得竞争);快速检测是否有其他线程在等待(比较head是否等于tail);release 状态是分散的,避免了内存竞争。

?

在原始版本的CLH Locknodes是没有被链起来的(见《线程锁系列(1):CLH Lock》)。但是如果每个node维护一个指向前驱的指针,CLH Lock可以处理timeout和一些cancel操作:如果一个node的前驱被cancle,这个node可以上滑使用前驱的状态字段。

?

为了使CLH Queues适合blocking synchronizers,一个额外的主要改动是提供node快速定位它的后继。在自旋锁里,node只需要改变自身状态,后继会在下一个自旋发现状态的改变。所以链接到后继是不必要的。但是在blocking synchronizers里,node需要明确的唤醒(unpark))它的后继。

?

一个AbstractQueuedSynchronizer queue node包含指向后继和前驱的指针。但是对于双向链表,没有使用compareAndSet,合适的lock-free,原子插入的算法。对指向后继的指针赋值不是插入操作的原子的一部分。它只是被简单的赋值:pred.next = node。插入后,会反映在所有的用法上。指向后继的指针只是一个优化的路径,如果一个后继好像不存在(可能被cancel),会定位到列表的tail,通过前驱指针,向上遍历,精确的检测是否有后继。

?

第二个变动是在每个node里使用一个状态字段去控制阻塞,而不是自旋。在framework里,一个排队的线程调用acquire,只有在通过了子类实现的tryAcquire才能返回。一个单独的“release”位是不能满足的。控制方法保证了只有在队里头部的活动线程才允许调用tryAcquire;在某些情况,线程可能acquire失败,重新block。这个保证不需要额外的状态,只需要检测node的前驱是否是head。和自旋锁不一样,通过复制减少了读head的内存冲突(And unlike the case of spinlocks, there is not enough memory contention reading head to warrant replication)。但是,cancle的状态需要在状态位描述。

?

Queue nodes 状态字段也会被用来避免不必要的parkunpark。当调用方法相当的快,它们有可能可以避免在jvmos中切换(parkunpark需要在jvmos中切换)。一个线程在调用park前,会把状态位设为“single me”,在park前再一次的检测状态位。一个释放lock的线程会清除这个状态位。这避免了不必要的block,通常来说,引入的额外开销也是值得的,特别是锁类,等待下一个获得线程的锁浪费了时间,加重竞争。这也避免了一个释放线程需要决定它的后继,除非后继状态位被设成“single me”。这可以消除后继指针为null的遍历开销,但是如果是cancel,开销仍然无法避免。

?

framework里的CLH Locks和其他语言实现的CLH Locks的最大不同可能是nodes的回收利用依赖于GC。它减少了复杂性和开销。但是即使依赖于GC,当指针被确定不需要再使用,仍然需要把它们置null,这通常发生在出队。否则仍然是可达的,导致不能被回收。

?

另外还有一些微小的改动,包括head使用的假节点延迟初始化。

?

忽略这些细节,一个基本的acquire实现(独占的,不可中断的,没有超时的)大体如下:

?

if (!tryAcquire(arg)) {
	node = create and enqueue new node;
	pred = node's effective predecessor;
	while (pred is not head node || !tryAcquire(arg)) {
		if (pred's signal bit is set)
			park();
		else
			compareAndSet pred's signal bit to true;
			pred = node's effective predecessor;
	}
		head = node;
}
?

Release操作如下:

?

if (tryRelease(arg) && head node's signal bit is set) {
	compareAndSet head's signal bit to false;
	unpark head's successor, if one exists
}
?

对于acquire,循环的次数依赖于tryAcquire,如果不考虑cancel和分摊parkos的线程调度,acquirerelease都是O1)操作。

?

为了支持cancel,在acquire循环中,需要检查超时和中断。一个由超时或中断引起的被取消的线程需要设置它的状态和unpark它的后继,所以它可能重置指针。随着cancel,决定前驱后继和重置状态,可能会包含On)的遍历(nQueue的长度)。因为一个已经被cancle的线程永远不可能再次阻塞,链接和状态需要被迅速的重建。