这里主要实现了多线程环境下的skiplist,读操作(count, find, skipper)都是lock free的,写操作(remove, add)也只是小范围的加了锁
主要的用法如下:
Sample usage:
typedef ConcurrentSkipList<int>SkipListT;
shared_ptr<SkipListT>sl(SkipListT::createInstance(init_head_height);
{
// It's usually good practice to hold anaccessor only during
// its necessary life cycle (but not ina tight loop as
// Accessor creation incurs ref-countingoverhead).
//
// Holding it longer delaysgarbage-collecting the deleted
// nodes in the list.
// Accessor提供了访问skip list的接口,我们不能直接使用skip list对象来访问数据
SkipListT::Accessor accessor(sl);
accessor.insert(23); // 增加节点
accessor.erase(2); // 删除节点
for (auto &elem : accessor) {
// use elem to access data
}
... ...
}
还有一种访问方式是Skipper,主要是用来跳过一部分数据,例如
{
SkipListT::Accessor accessor(sl);
SkipListT::Skipper skipper(accessor);
skipper.to(30); // 跳到比30大的第一个节点
if (skipper) {
CHECK_LE(30, *skipper);
}
... ...
// GC may happen when the accessor getsdestructed.
}
我们这里重点从Accessor来分析一下查找、增加和删除的流程:
查找:
typedef detail::csl_iterator<value_type, NodeType> iterator; // 利用boostiterator_facade生成的iterator
iterator find(const key_type &value);
- 调用skip list的find方法
- 调用findNode方法,如果找到节点并且该节点没有markedForRemoval的话就返回,否则返回nullptr
- 调用findNodeDownRight(查找时先向下遍历,然后再向右遍历)方法,这里值得说一下的是,skip list里还实现了一个findNodeRightDown(查找时先向右遍历,然后再向下遍历)方法,但性能不如findNodeDownRight,因为后者的cache locality要更好一些
- 我们来看下findNodeDownRight方法是怎么实现的:
std::pair<NodeType*, int>findNodeDownRight(const value_type &data) const {
NodeType *pred = head_.load(std::memory_order_consume); // 从head开始查找
int ht = pred->height();
NodeType *node = nullptr;
bool found = false;
while (!found) {
// stepping down,直到找到一个节点pred的数据比data大
for (; ht > 0 && less(data,pred->skip(ht - 1)); --ht) {}
if (ht == 0) returnstd::make_pair(pred->skip(0), 0); //not found
node = pred->skip(--ht); // node <= data now
// stepping right,继续接近data
while (greater(data, node)) {
pred = node;
node = node->skip(ht);
}
found = !less(data, node);
}
return std::make_pair(node, found);
}
增加:
std::pair<iterator,bool> insert(const key_type &data)
- 调用skip list的addOrGetData(data)方法
- 我们来看下addOrGetData的实现
std::pair<NodeType*,size_t> addOrGetData(const value_type &data) {
NodeType *preds[MAX_HEIGHT],*succs[MAX_HEIGHT];
NodeType *newNode;
size_t newSize;
while (true) {
int max_layer = 0;
// 找到data对应的节点,以及它的前继和后继,max_layer返回当前skip list的最大层级
// 返回值layer是data对应的节点备找到时的layer
int layer =findInsertionPointGetMaxLayer(data, preds, succs, &max_layer);
if (layer >= 0) {// 如果找到
NodeType *nodeFound = succs[layer];
DCHECK(nodeFound != nullptr);
if (nodeFound->markedForRemoval()) {
continue; // if it's getting deleted retry findingnode.
}
// wait until fully linked. 可能节点被其他线程加入了,暂时还没有fully linked
// 等待完成后再返回给用户完整的节点
while(UNLIKELY(!nodeFound->fullyLinked())) {}
return std::make_pair(nodeFound, 0);
}
// need to capped at the original height-- the real height may have grown
// 按概率生成新的节点高度,新节点的高度上限设为max_layer+1
// 值得注意的是选取概率是1/e
int nodeHeight =detail::SkipListRandomHeight::instance()->
getHeight(max_layer + 1);
ScopedLocker guards[MAX_HEIGHT];
// 把前继全部加上锁
if (!lockNodesForChange(nodeHeight,guards, preds, succs)) {
continue; // give up the locks andretry until all valid
}
// locks acquired and all valid, need tomodify the links under the locks.
// 按照生成的高度建立新的节点
newNode = NodeType::create(nodeHeight,data);
// 把新的节点联入skip list中
for (int layer = 0; layer <nodeHeight; ++layer) {
newNode->setSkip(layer,succs[layer]);
preds[layer]->setSkip(layer,newNode);
}
// 标记fully linked
newNode->setFullyLinked();
newSize = incrementSize(1);
break;
}
int hgt = height();
size_t sizeLimit =
detail::SkipListRandomHeight::instance()->getSizeLimit(hgt);
// 检查是否需要增加skip list节点的高度
if (hgt < MAX_HEIGHT && newSize> sizeLimit) {
growHeight(hgt + 1);
}
CHECK_GT(newSize, 0);
return std::make_pair(newNode, newSize);
}
- 我们再分别来看这个函数中调用的几个方法
- 先来看findInsertionPointGetMaxLayer
- 它首先把skip list的高度返回为max_layer
- 然后按照right down的方式查找节点,不同的是在查找过程中会保留前继指针preds[]和后继指针succs[]
- 再来看看SkipListRandomHeight::getHeight方法和SkipListRandomHeight::getSizeLimit方法
- 在SkipListRandomHeight构造的时候会初始化两张表
- lookupTable_:高度的概率表,高度1的概率是1-e,高度2的概率是(1-e)*e,高度3的概率是(1-e)*e*e以此类推
- sizeLimitTable_:skip list的高度对应的最大的list size,高度1的size为1,高度2的size为1*e,高度3的size为1*e*e,以此类推
- 回到getHeight方法,这里用随机函数生成一个0~1之间的double值p,然后在lookupTable中找比p大的值对应的表索引i,找到后获得的高度就是i+1
- getSizeLimit方法也类似,以参数height为sizeLimitTable的索引,返回对应高度的sizeLimit
- 接下来看看lockNodesForChange方法,我们来看下代码
boollockNodesForChange(int nodeHeight,
ScopedLocker guards[MAX_HEIGHT],
NodeType *preds[MAX_HEIGHT], // 插入或删除节点的前继
NodeType *succs[MAX_HEIGHT], // 插入或删除节点的后继
bool adding=true) {// adding为true表明该函数是在add里备调用,否则是在remove里被调用
NodeType *pred, *succ, *prevPred = nullptr;
bool valid = true;
for (int layer = 0; valid && layer< nodeHeight; ++layer) {
pred = preds[layer];
DCHECK(pred != nullptr) <<"layer=" << layer << " height=" <<height()
<< " nodeheight="<< nodeHeight;
succ = succs[layer];
if (pred != prevPred) { // 可能连续多层的前继指针都是一个节点,这里可以避免多次上锁
guards[layer] =pred->acquireGuard();
prevPred = pred;
}
// 对于remove来说只要判断前继没有被删除并且前继的后继是后继节点即可
valid = !pred->markedForRemoval()&&
pred->skip(layer) == succ; // check again after locking
// 对于adding来说还需要判断后继节点没有被删除
if (adding) { // when adding a node, the succ shouldn't begoing away
valid = valid && (succ ==nullptr || !succ->markedForRemoval());
}
}
return valid;
}
d.最后看看下growHeight(int height)方法,主要逻辑是替换head节点
void growHeight(int height) {
NodeType* oldHead =head_.load(std::memory_order_consume);
if (oldHead->height() >= height){ // someone else already did this
return;
}
// 生成新的head节点,height参数就是在原来的heigth基础上加1
NodeType* newHead =NodeType::create(height, value_type(), true);
{ // need to guard the head node in caseothers are adding/removing
// nodes linked to the head.
ScopedLocker g =oldHead->acquireGuard();
newHead->promoteFrom(oldHead); // 从oldHead中把数据拷贝过来,类似拷贝构造函数
NodeType* expected = oldHead;
// 原子替换head_指针指向newHead
if(!head_.compare_exchange_strong(expected, newHead,
std::memory_order_release)) {
// if someone has already done theswap, just return.
NodeType::destroy(newHead);
return;
}
oldHead->setMarkedForRemoval();
}
// 加入gc,具体流程在
recycle(oldHead);
}
删除:
bool remove(constkey_type &data)
- 实际调用的是skip list的remove方法
- 我们来分析一下remove的源码
bool remove(const value_type &data) {
NodeType *nodeToDelete = nullptr;
ScopedLocker nodeGuard;
bool isMarked = false; //表示是否已经完成对删除节点的标记(markForRemoval)
int nodeHeight = 0;
NodeType* preds[MAX_HEIGHT],*succs[MAX_HEIGHT];
while (true) {
int max_layer = 0;
// 先找到要删除的节点,以及它的前继和后继,max_layer返回当前skiplist的最大层级
// 返回值layer是data对应的节点备找到时的layer
int layer =findInsertionPointGetMaxLayer(data, preds, succs, &max_layer);
// okToDelete的判断条件是:节点fully linked,节点没有被标记markedForRemoval,
// 并且节点的top layer和查到节点的layer一致,否则说明该节点是partailly linked
if (!isMarked && (layer < 0 ||!okToDelete(succs[layer], layer))) {
return false;
}
// 给要删除的节点设置markedForRemoval
if (!isMarked) {
nodeToDelete = succs[layer];
nodeHeight = nodeToDelete->height();
nodeGuard =nodeToDelete->acquireGuard();
if(nodeToDelete->markedForRemoval()) return false;
nodeToDelete->setMarkedForRemoval();
isMarked = true;
}
// acquire pred locks from bottom layerup
// 获取所有前继的锁
ScopedLocker guards[MAX_HEIGHT];
if (!lockNodesForChange(nodeHeight,guards, preds, succs, false)) {
continue; // this will unlock all the locks
}
// 修改前继指针,删除对应节点
for (int layer = nodeHeight - 1; layer>= 0; --layer) {
preds[layer]->setSkip(layer,nodeToDelete->skip(layer));
}
incrementSize(-1);// 这里不会降低高度
break;
}
// 把节点加入gc中
recycle(nodeToDelete);
return true;
}
- 这里值得提一下的是Recycler对象,他负责回收被删除的节点,但其实它只是把节点加入一个vector,然后在Recycler对象析构或者显示调用release方法时才会去释放这些节点
本文深入探讨了Folly库中的ConcurrentSkipList,重点分析了在多线程环境中,其无锁查找、加锁修改的实现细节。通过sample usage展示了查找、插入和删除操作的流程,包括findNodeDownRight方法的优化,addOrGetData函数中的findInsertionPointGetMaxLayer、SkipListRandomHeight的height和sizeLimit计算,以及lockNodesForChange和删除操作涉及的Recycler机制。
812

被折叠的 条评论
为什么被折叠?



