解决方案可能是使用atomic operations。没有锁定,没有上下文切换,没有睡眠,并且比互斥锁或条件变量快得多。原子操作并不是所有问题的最终解决方案,但我们已经创建了许多仅使用原子操作的通用数据结构的线程安全版本。他们非常快。
原子操作是一系列简单的操作,如递增、递减或赋值,保证在多线程环境中原子执行。如果两个线程同时命中操作,cpu 会确保一个线程一次执行操作。原子操作是硬件指令,所以它们很快。 “比较和交换”对于线程安全的数据结构非常有用。在我们的测试中,原子比较和交换大约与 32 位整数赋值一样快。也许慢 2 倍。考虑到互斥体消耗了多少 CPU 时,原子操作的速度无限快。
用原子操作来平衡你的树并不是一件容易的事,但也不是不可能的。我过去遇到过这个要求,并通过使线程安全skiplist 作弊,因为可以通过原子操作轻松完成跳过列表。抱歉,我不能给你一份我们的代码的副本……我的公司会解雇我,但你自己做很容易。
可以通过简单的线程安全链表示例来可视化原子操作如何工作以生成无锁数据结构。在不使用锁的情况下将项目添加到全局链表 (_pHead)。首先保存一份_pHead, pOld。在执行并发操作时,我将这些副本视为“世界状态”。接下来创建一个新节点 pNew,并将其 pNext 设置为 COPY。然后使用原子“比较和交换”将 _pHead 更改为 pNew,仅当 pHead 仍然是 pOld 时。仅当 _pHead 未更改时,原子操作才会成功。如果失败,则循环返回以获取新 _pHead 的副本并重复。
如果操作成功,世界其他地方现在将看到一个新的负责人。如果一个线程在一纳秒之前获得了旧的头,该线程将不会看到新项目,但列表仍然可以安全地迭代。由于我们在将新项目添加到列表之前将 pNext 预设为旧头,因此如果线程在我们添加新头后一纳秒内看到新头,则可以安全地遍历列表。
全球的东西:
typedef struct _TList {
int data;
struct _TList *pNext;
} TList;
TList *_pHead;
加入列表:
TList *pOld, *pNew;
...
// allocate/fill/whatever to make pNew
...
while (1) { // concurrency loop
pOld = _pHead; // copy the state of the world. We operate on the copy
pNew->pNext = pOld; // chain the new node to the current head of recycled items
if (CAS(&_pHead, pOld, pNew)) // switch head of recycled items to new node
break; // success
}
CAS 是__sync_bool_compare_and_swap 等的简写。看看有多容易?没有互斥锁...没有锁!在极少数情况下,两个线程同时访问该代码,一个简单地循环第二次。我们只看到第二个循环,因为调度程序在并发循环中交换了一个线程。所以它很少见且无关紧要。
可以以类似的方式将事物从链表的头部拉出。如果您使用联合,您可以原子地更改多个值,并且您可以使用最多 128 位的原子操作。我们在 32 位 redhat linux 上测试了 128 位,它们的速度与 32、64 位原子操作的速度相同。
您必须弄清楚如何将这种技术用于您的树。一个 b 树节点将有两个指向子节点的 ptr。您可以对它们进行 CAS 更改。平衡问题很棘手。我可以看到您如何在添加某些内容并从某个点复制分支之前分析树枝。当您完成更改分支时,您将新分支放入。这对于大型分支来说将是一个问题。当线程没有争夺树时,也许可以“稍后”完成平衡。也许你可以做到这一点,即使你没有一直级联旋转,树仍然是可搜索的……换句话说,如果线程 A 添加了一个节点并且递归地旋转节点,线程 b 仍然可以读取或添加节点。只是一些想法。在某些情况下,我们会在 pNext 的 32 位之后的 32 位中创建一个具有版本号或锁定标志的结构。然后我们使用 64 位 CAS。也许您可以使树在任何时候都可以安全地读取而无需锁定,但您可能必须在正在修改的分支上使用版本控制技术。
以下是我发表的一些关于原子操作优势的帖子:
Pthreads and mutexes; locking part of an array
Efficient and fast way for thread argument
Configuration auto reloading with pthreads
Advantages of using condition variables over mutex
single bit manipulation
Is memory allocation in linux non-blocking?