下面是一个简化的C程序,演示了我使用内置比较和在intel cpu上交换的GNU实现并发堆栈的问题。我花了一段时间来了解发生了什么,但现在我知道它完全符合原子比较和交换所提供的保证。
当一个节点从堆栈中弹出,被修改,然后放回堆栈时,修改后的值可能会成为堆栈的新头,破坏它。 test_get中的注释描述了导致此事件发生的事件的顺序。
有没有办法可以多次可靠地使用相同堆栈的同一节点?这是一个夸张的例子,但即使将未修改的节点返回给gHead也可能泄漏其他节点。该数据结构的原始要点是重复使用相同的节点。
typedef struct test_node {
struct test_node *next;
void *data;
} *test_node_p;
test_node_p gHead = NULL;
unsigned gThreadsDone = 0;
void test_put( test_node_p inMemory ) {
test_node_p scratch;
do {
scratch = gHead;
inMemory->next = scratch;
} while ( !__sync_bool_compare_and_swap( &gHead , scratch , inMemory ) );
}
test_node_p test_get( void ) {
test_node_p result;
test_node_p scratch;
do {
result = gHead;
if ( NULL == result ) break;
// other thread acquires result, modifies next
scratch = result->next; // scratch is 0xDEFACED...
// other thread returns result to gHead
} while ( !__sync_bool_compare_and_swap( &gHead , result , scratch ) );
// this thread corrupts gHead with 0xDEFACED... value
if ( NULL == result ) {
result = (test_node_p)malloc( sizeof(struct test_node) );
}
return result;
}
void *memory_entry( void *in ) {
test_node_p node;
int index , count = 1000;
for ( index = 0 ; index < count ; ++index ) {
node = test_get();
*(uint64_t *)(node) |= 0xDEFACED000000000ULL;
test_put( node );
}
__sync_add_and_fetch(&gThreadsDone,1);
return NULL;
}
void main() {
unsigned index , count = 8;
pthread_t thread;
gThreadsDone = 0;
for ( index = 0 ; index < count ; ++index ) {
pthread_create( &thread , NULL , memory_entry , NULL );
pthread_detach( thread );
}
while ( __sync_add_and_fetch(&gThreadsDone,0) < count ) {}
}
我正在使用8个逻辑核心运行此测试。当我只使用4个线程时,问题很少发生,但是8个很容易重现。我有一台配备英特尔酷睿i7的MacBook。
我对使用已解决此问题的某些库或框架不感兴趣,我想知道它是如何解决的。谢谢。
编辑:
以下两种解决方案在我的案例中不起作用。
某些体系结构提供了对地址而不是值执行原子测试的ll / sc指令对。对地址的任何写入,即使是相同的值,都会阻止成功。
某些体系结构提供比指针大小更大的比较和交换。有了这个,单调计数器与指针配对,每次使用它时都会原子递增,因此值总是会改变。一些英特尔芯片支持这一点,但没有GNU包装器。
这是一个播放问题的游戏。两个线程A和B到达test_get
中的点,result
具有相同的值,而不是NULL
。然后发生以下序列:
result
test_get
result
result
,获取A放置的任何线程result
结束并调用test_put
test_put
将结果放回gHead
test_get
并传递gHead
醇>
以下是三个线程的类似场景,不需要线程A来修改result
。
result
test_get
result
result
,在scratch
test_put
并成功test_put
并成功将result
重新放回gHead
test_get
并传递gHead
醇>
在任何一种情况下,问题是线程A传递比较并交换两次,一次用于获取然后再次用于put,在线程B到达比较并交换get之前。应该丢弃线程B用于临时的任何值,但不是因为gHead中的值看起来是正确的。
任何能够在不实际阻止它的情况下降低这种可能性的解决方案只会使bug更难以追踪。
请注意,scratch变量只是在原子指令开始之前放置结果的解除引用值的位置。删除名称会删除可能被中断的取消引用和比较之间的时间片。
另请注意,原子意味着整体成功或失败。对指针的任何对齐读取在所讨论的硬件上是隐式原子的。就其他线程而言,没有可中断的点,只有一半的指针被读取。
答案 0 :(得分:4)
永远不要通过简单的评估来访问原子变量。另外,对于像你这样的比较和交换循环,我认为__sync_val_compare_and_swap
更方便。
/* read the head atomically */
result = __sync_val_compare_and_swap(&gHead, 0, 0);
/* compare and swap until we succeed placing the next field */
while ((oldval = result)) {
result = __sync_val_compare_and_swap(&gHead, result, result->next);
if (oldval == result) break;
}
答案 1 :(得分:2)
(我放弃了之前的回答。)
问题在于您没有自动阅读gHead
和gHead->next
的机制,但需要这样才能实现无锁堆栈。既然你打算忙着循环来处理比较和交换冲突,你可以使用等效的自旋锁:
void lock_get () {
while (!_sync_bool_compare_and_swap(&gGetLock, 0, 1)) {}
}
void unlock_get () {
unlock_get_success = _sync_bool_compare_and_swap(&gGetLock, 1, 0);
assert(unlock_get_success);
}
现在,test_get()
中的循环可以被lock_get()
和unlock_get()
包围。 test_get()
的CAS循环只是一个与test_put()
竞争的线程。 Jens对CAS循环的实现似乎更清晰。
lock_get();
result = __sync_val_compare_and_swap(&gHead, 0, 0);
while ((oldval = result)) {
result = __sync_val_compare_and_swap(&gHead, result, result->next);
if (oldval == result) break;
}
unlock_get();
这实现了意图,即只有一个线程应该弹出头部。
答案 2 :(得分:1)
如果您有CAS变量(在您的情况下为gHead)。您必须始终使用CAS来访问它。或用锁保护它。用于阅读和写作。像&#34;结果= gHead;&#34;是一个很大的禁忌。
重新阅读你的问题,LIFO是一个堆栈。实现任何基于CAS的数据结构都基于只有一件事要改变。在堆栈顶部的堆栈中。你好像在做一个链表。我确信有很酷的方法来做原子链表。
但对于堆栈,请像其他人一样做一个堆栈指针:)