Lock-free 多核数据结构设计

来源:互联网 发布:淘宝店铺复制宝贝 编辑:程序博客网 时间:2024/06/05 18:47

lock-free思想背景

基本的多核数据结构设计是非常简单的:只需要在并发处理同一数据结构时,加上locks就可以了。这种思想在并发数不是很多的情况下工作的很好。因为这时的资源争用开销并不是很大。

随着多核机器逐渐的变大变多,例如成百上千的核,这种加lock机制对此应用的不是很好:如果你只有几个锁,锁的争夺成为巨大的;如果你有许多细粒度的锁,锁的开销开始增加。

所以对于大型多核机器来说,“Lock-free”的设计非常常见。以下这些策略基本覆盖了这种设计思想:

1、我们不每次都是用lock,但只有当我们真的需要它们的时候(例如只写不读);

2、我们不是每秒都用lock,但是我们可以自己构建相同的东西(loop  until free[不知翻译成什么好]);

3、我们不使用lock,相反我们采用原子指令操作;

4、我们不使用lock,因为我们假设并发线程不同时访问同一数据;

5、我们不使用lock,因为我们可以证明多线程从来不在同一时间访问同一数据


一个非常严重而且非常复杂的因素就是机器的“内存一致性模型”:如何使一个线程正在写的数据同时允许另一个线程查看。如果你把数据先后写在内存的两个地方A,然后是B。一些线程开始从B处读取你的新数据,你可能期望它们也会那样在A进行相同的操作。不幸的是,你写在A内存块中的数据很有可能被放在了存储缓冲区中,所以导致B处写的数据要早于A出现。因此,所有的机器都要求设置“内存栅栏”操作来确保所有的写操作都对外可见。

Example:Lock-Free Hashtable 设计

这里有一个很好的例子on Dr. Cliff Click's lock-free hashtable(in Java,for "Azul" native Java 54-core/chip massive muticore hardware)。Dr Cliff Click思想很简单直接,他把内存一致性模型暴露给了用户。lock free的写操作时很复杂的,特别是重新调整hashtable的大小时。


以下是Dr. Lawlor采取Cliff的思想,但是去掉了最棘手的调整操作。这些代码只适合于小测试。

#include <omp.h>/*For performance debugging: count hashtable collisions*/int collisions = 0;/*A lockness hashtable. Both reads and writes are lock-free.Does not yet implement hashtable resize.*/template <class KEY, class VALUE>class LockFreeHashtable {KEY missingk; // invalid keyVALUE missingv; // invalid keylong size;struct KEY_VALUE{KEY k;VALUE v;};volatile KEY_VALUE *data;void operator=(const LockFreeHashtable &){} // do not copy usepublic:LockFreeHashtable(long size_, KEY missingk=KEY(), VALUE missingv=VALUE()):missingk(missingk_), missingv(missingv_), size(0), data(0) {reinit(size_);}~LockFreeHashtable() {delete[] data;}/* Reinitialize to be this new size*/void reinit(long size_) {delete[] data;size = size_;data = new KEY_VALUE[size];for (int i = 0; i < size; ++i) {data[i].k = missingk;data[i].v = missingv;}}/*read the VALUE for the KEY*/volatile const VALUE &get(consts KEY &k) {int idx = k;while (true) { /*while we haven't found that key yet*/idx &= size - 1; /*===     idx %= size; */if (data[idx].k == k) { /*old key*/return data[idx].v;}if (data[idx].k == missingk) { /*missing key*/return missingv;}idx ++; /*move down: keep looking !*/#pragma omp atomiccollisions ++;}}/*Writes a copy of Value for this KEY*/void put(const KEY &k, const VALUE &v) {int idx = k;while(true) {idx &= size -1; /*===   idx %= size;  */check_key:if (data[idx].k == k) {/*fast path:resue old key*/data[idx].v = v;return;}if (data[idx].k == missingk) { /*try to claim new key*/data[idx].k = k; /*provisinal ownership*/goto check_key; /*subtle: check ownership below *before* use */}idx ++; // move down: keep looking;#pragma omp atomiccollisions ++;}}/*Count number of valid values in table */int count (void) {int sum = 0;for (int i = 0; i < size; ++i) {if (data[i].k != missingk) sum ++;}return sum;}};LockFreeHashtable<int, float> h(0, -1, -99.0);enum {n = 100000}; /*total number of hashtable operations*/inline int make_key(int i) {i *= 8193; /*randomizing function*/return i ^ (i >> 16);}int do_hashtable_writes(void) {collisions = 0;#pragma omp parallel forfor (int i = 0; i < n; ++i) {h.put(make_key(i), i * 1.23456);}return 0;}int do_hashtable_reads(void) {collisions = 0;double sum = 0.0;#pragma omp parallel for reduction(+:sum)for (int i = 0; i < n; ++i) {sum += h.get(make_key(i));}return sum;}int foo(void) {for (int nthread = 1; nthread <= 4; nthread *=2) {h.reinit(1024 * 512);printf("%d threads : \n", nthread);omp_set_num_threads(nthread);double t = time_function(do_hashtable_writes);printf("writes: %.3f ns per (%.3f ms total)\n",t*1.0e9/n,t*1.0e3);std::cout <<"collisions: "<< collisions <<"\n";std::cout <<"total values: "<< h.count() <<"\n";t = time_function(do_hashtable_reads);printf("reads: %.3f ns per (%.3f ms total)\n",t*1.0e9/n,t*1.0e3);std::cout << "collisions: " << collisions << "\n";}return 0;}

在collisions很少的情况下,可伸缩性还是很好的微笑

上面的例子对于int类型和float类型是很安全的,但是对于动态分配的对象而言,“ABA问题”使回收很棘手——Java的解决方案就是垃圾收集,有一个较好的解决方案叫 Hazard Pointers

原创粉丝点击