Lock-free 多核数据结构设计

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 key
	VALUE missingv; // invalid key

	long size;
	struct KEY_VALUE{
		KEY k;
		VALUE v;
	};

	volatile KEY_VALUE *data;
	void operator=(const LockFreeHashtable &){} // do not copy use
public:
	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 atomic
			collisions ++;
		}
	}

	/*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 atomic
			collisions ++;
		}
	}

	/*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 for
	for (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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值