无锁数据结构(四)

 

无锁数据结构(四)

Andrei Alexandrescu

December 16, 2007

译者:张桂权

12/25/2007

初稿阶段,没有得到许可不得引用,否则后果自负

6 写锁(Write-LockedWRRM Maps

为了了解敌人的邪恶,首先尝试一个经典的引用计数实现,并弄清失败的原因是非常具有教育意义的。所以,让我们思考一个使用map指针的引用计数,用WRRMMap存储一个指向这种形式的机构的指针。

template <class K, class V>

class WRRMMap {

typedef std::pair<Map<K, V>*,

unsigned> Data;

Data* pData_;

...

};

美极了。现在,Lookup对pData_->second加1,然后查找map中所有它想要的项,最后把pData_->second减1。当引用计数值为0时,pData_->first可以被delete了,然后pData_自身也被delete了。似乎连傻子都懂(foolproof),但是……

但是,这实在太“愚蠢(foolful)”了(或“连傻子都懂”的任意反义词)。试想,正当某个线程发现引用计数值为0,并且开始删除pData_时,另外一个线程……不,更好点:一个bazillion线程已经锁住垂死的pData_,并且准备读取其中的数据项。无论你的方案有多灵巧,都将碰上这个基础的catch-22(竞争):读取指向数据的指针,一个需要增加引用计数;但是计数器必须是数据本身的一部分,所以没有访问指针之前它不能读取数据。这有点像电子栅栏,在其顶端有一个开-关按钮:为了安全的攀越栅栏,你首先需要关掉它,但是关掉它的目的不是你需要攀越它。

所以,让我们思考其他的方法来恰当的delete旧的map。一个解决方案是等待,然后delete。旧的pMap_对象在处理器运行一毫秒之内将会被少之又少的线程查找;这是因为新的查找将使用新的map,一旦CAS之间活动的查找结束,pMap_已经准备好Hades(倾斜)了。因此,一个方案应该在一个循环中给某个“蟒蛇”(boa serpent)线程排列旧的pMap_的值。这个线程休眠,大约,200毫秒,然后唤醒,delete最近的map,进入消化休眠。

这不是一个理论上安全的方案(虽然,实际上在边界之内很不错)。不管出于什么理由,最邪恶的一件事情是,如果一个查找线程被延迟太久了,“蟒蛇”线程将在这个线程的脚下delete这个map。可以通过一直给“蟒蛇”线程赋一个低于其它任何线程的优先级,但是作为一个整体的方案,有了“臭气”之后,就很难消除了。如果你也同意,用严肃的表情很难防御这种技术,那就让那个我们继续吧。

另外一个方案[4],依赖于一个经过扩展的DCAS原子指令,它可以在内存中比较和交换两个非共边(non-contiguous)的字。

template <class T1, class T2>

bool DCAS(T1* p1, T2* p2,

T1 e1, T2 e2,

T1 v1, T2 v2) {

if (*p1 == e1 && *p2 == e2) {

*p1 = v1; *p2 = v2;

return true;

}

return false;

}

很自然了这两个位置是指针和引用技术本身。DCAS已经在Motorola 68040处理器上实现非常的低效),而不是其它的处理器。因为基于DCAS的方案被认为只有理论价值。

第一发子弹目标是一个具有确定性解构的方案,依赖于很少需求的CAS2。前面曾经提到,很多32位机器实现了64位的CAS,通常被称作CAS2。(因为它仅操作共边的字,很显然CAS2没有DCAS有效。)为了启动器,我们将引用计数的值保存到它看守的指针之后:

template <class K, class V>

class WRRMMap {

typedef std::pair<Map<K, V>*,

unsigned> Data;

Data data_;

...

};

注意了现在我们把计数保存到它保护的指针的后面这样我们就可以摆脱前面提到的catch-22问题了。我们将看它在启动一分钟内的代价。)

当访问map之前,我们改变Lookup来给引用计数加1,之后进行减1。为了简明扼要,在下面的代码段中,我们将忽略异常安全问题(谨慎使用标准计数可以实现):

V Lookup(const K& k) {

Data old;

Data fresh;

do {

old = data_;

fresh = old;

++fresh.second;

} while (CAS(&data_, old, fresh));

V temp = (*fresh.first)[k];

do {

old = data_;

fresh = old;

--fresh.second;

} while (CAS(&data_, old, fresh));

return temp;

}

最后Update用一个新的来替换这个map——但是仅当引用计数为1时有机会窗口的才可以。

void Update(const K& k,

const V& v) {

Data old;

Data fresh;

old.second = 1;

fresh.first = 0;

fresh.second = 1;

Map<K, V>* last = 0;

do {

old.first = data_.first;

if (last != old.first) {

delete fresh.first;

fresh.first =new Map<K, V>(old.first);

fresh.first->insert(make_pair(k, v));

last = old.first;

}

} while (!CAS(&data_, old, fresh));

delete old.first; // whew(嚄, 哨声)

}

这里Update是如何工作的呢我们使用到现在很熟悉的变量oldfresh。但是现在的old.second(计数)从来不会通过data_.second赋值;它一直都是1。这就意味着,Update将一直循环,直到它有一个用另外一个拥有计数器值为1的指针来替换指向一个计数器的值为1的指着的机会的窗口。用浅显的英语来说,这个循环会说“我将用一个新的map替换这个旧的,更新了一个之后,我将开始禁戒其它任何一个更新这个map,但是当且仅当只有一个map存在的时候我才进行替换。”变量last和相关的代码仅仅是一个优化:避免在旧的map还没有替换完(仅这计数器),而反反复复的重新构建map。

够整洁吧?差远了。Update现在加锁了:在有机会更新map之前,它需要等待所有的Lookup结束。随风飘扬是无锁数据结构最好性质。尤其是,很容易使Update饿死(starve Update to death):足够高的频率查找map——但是引用计数将永远都不会降到1。所以到目前为止我们得到的不是一个WRRM(Write-Rarely-Read-Many,少写多读)map,而是一个WRRMBNTM(Write-Rarely-Read-Many-But-Not-Many,少写多读,但不是太多)map。



---------------源文档------------------
Lock-Free Data Structures
Andrei Alexandrescu
December 17, 2007
http://erdani.org/publications/cuj-2004-10.pdf
最后一次访问时间:2007年12月30日
----------------------------------------
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值