【发布时间】:2013-06-19 08:19:23
【问题描述】:
下面是一个简化的 C 程序,它演示了我在使用 GNU 内置的比较和交换在英特尔 cpu 上实现的并发堆栈时遇到的问题。我花了一段时间才明白发生了什么,但现在我明白了,这完全在原子比较和交换提供的保证范围内。
当一个节点从堆栈中弹出,修改,然后放回堆栈时,修改后的值可能成为堆栈的新头部,从而破坏它。 test_get 中的 cmets 描述了导致这种情况的事件顺序。
有没有办法可靠地多次使用具有相同堆栈的相同节点?这是一个夸张的例子,但即使将未修改的节点返回给 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 个线程很容易重现。我有一台配备 Intel Core i7 的 MacBook。
我对使用已解决此问题的某些库或框架不感兴趣,我想知道它是如何解决的。谢谢。
编辑:
这里有两个在我的情况下不起作用的解决方案。
某些架构提供 ll/sc 指令对,它们对地址而不是值执行原子测试。对该地址的任何写入,即使是相同的值,也会阻止成功。
一些架构提供大于指针大小的比较和交换。有了这个,单调计数器与每次使用时原子递增的指针配对,因此值总是改变。一些英特尔芯片支持这一点,但没有 GNU 包装器。
这里是逐个播放的问题。两个线程 A 和 B 到达test_get 中的点,它们在result 中具有相同的值,但不是NULL。然后出现以下顺序:
- 线程 A 通过比较和交换并从
test_get返回result - 线程A修改
result的内容 - 线程 B 取消引用
result,得到任何线程 A 放在那里 - 线程 A 以
result结束并调用test_put - 线程 A 通过
test_put中的比较和交换将结果返回到gHead - 线程 B 到达
test_get中的比较和交换并通过 - 线程 B 现在已使用来自线程 A 的值损坏了
gHead
这是一个类似的场景,三个线程不需要线程 A 修改result。
- 线程 A 通过比较和交换并从
test_get返回result - 线程A没有修改
result的内容 - 线程 B 取消引用
result,在scratch中获取有效值 - 线程 C 使用不相关的值调用
test_put并成功 - 线程 A 调用
test_put并成功将result放回gHead - 线程 B 到达
test_get中的比较和交换并通过 - 线程 B 现在已损坏
gHead,泄漏了添加的任何线程 C
在任何一种情况下,问题都是线程 A 通过了比较和交换两次,一次是 get,然后是 put,在线程 B 到达比较和交换之前,是 get。线程 B 的任何从头开始的值都应该被丢弃,但这并不是因为 gHead 中的值看起来正确。
任何在不实际阻止的情况下降低这种可能性的解决方案只会使错误更难追踪。
请注意,临时变量只是在原子指令开始之前放置结果的取消引用值的位置的名称。删除名称确实会删除可能被中断的取消引用和比较之间的时间片。
还要注意,原子意味着它作为一个整体成功或失败。指针的任何对齐读取在相关硬件上都是隐式原子的。就其他线程而言,不存在只读取一半指针的可中断点。
【问题讨论】:
标签: c concurrency atomic compare-and-swap lifo