考虑以下使用非阻塞语义来弹出堆栈的代码:
T Stack<T>::pop( )
{
while (1) {
if (top == NULL)
throw std::string(“Cannot pop from empty stack”);
Node* result = top;
if (top && __sync_bool_compare_and_swap(&top, result, top->next)) {
return result->data;
}
}
}
我担心的是,如果执行pop的线程在第二个if语句之前被取消调度,并且时间到了时间片,那么堆栈的空是我的第二个循环检查是否足以避免崩溃?当然,在最坏的情况下,在将top与零进行比较之后,线程可能会被取消调度。
任何意见赞赏。我知道也可能发生ABA问题。
答案 0 :(得分:2)
首先,假设top
是易变的,并且可以在任何时候被另一个线程更改,每个循环只取一次它的值,这样你就不会从你的下方拉出地毯了:
T Stack<T>::pop( )
{
while ( 1 ) {
Node* result = top;
if ( result == NULL )
throw std::string ( “Cannot pop from empty stack” );
// you now know result isn't NULL here
if ( __sync_bool_compare_and_swap ( &top, result, result -> next ) ) {
return result -> data;
}
}
}
这仍然无法解决result
在获取top
的值与解除引用之间被删除或以其他方式修改的问题。
你想使用安全的哨兵而不是result -> next
,所以逻辑是:
这是否仍然算作等待†取决于你是否能在中间状态下找到有用的东西。
有很多论文要比使用哨兵更有效地阅读 - 实际上你是用一个CAS模拟一个双字CAS,因为你需要检查一下result
的状态。以及top
的状态。这些太复杂了,无法在这里复制。
未经任何方式测试:
bool Stack<T>::pop ( T&out )
{
static const Node* const empty ( 0 );
static const Node* const sentinel ( empty + 1 );
while ( true ) {
Node* result = top;
if ( result == empty )
throw std::string ( “Cannot pop from empty stack” );
if ( result == sentinel )
// something else is popping, return false and allow
// current thread to do some work before retrying
return false;
if ( __sync_bool_compare_and_swap ( &top, result, sentinel ) ) {
// only one thread can CAS from a given result to something,
// so we are the only thread which has this value of result
// hence we can dereference it and delete it/return it to a pool
//
// no other thread will change top if top == sentinel, so this
// CAS can't fail
if ( !__sync_bool_compare_and_swap ( &top, sentinel, result -> next ))
throw std::string ( "nobody's perfect" );
out = result -> data;
delete result;
return true;
}
}
}
由于你一次只检查或改变一个线程中结果的指针,它应该是安全的(我以前没有使用过这个确切的模式,通常我最后会想到奇怪的情况几天后我设计了一些东西)。这是否比使用pthread_mutex_trylock包装std :: deque更好,值得测量。
当然,无论是这个还是原始的都不是非阻塞的 - 如果一个线程不断拉出堆栈,任何其他线程将无限期地绕过循环等待CAS成功。如果CAS确实失败了,那么通过返回false可以很容易地删除,但是如果它不应该等待,你必须弄清楚你想让线程做什么。如果旋转直到某些东西可以出队是可以的,你不需要返回代码。
†我主要使用x86 / x64,其中没有无锁代码,因为CMPXCHG隐式锁定总线并占用与要同步的高速缓存数量成比例的时间。所以你可以拥有不旋转和等待的代码,但你不能拥有不锁定的代码。
答案 1 :(得分:1)
如何改变一些东西,以便你抓住堆栈顶部的任何东西,如果事实证明堆栈的顶部是空的,那么引发异常。
答案 2 :(得分:0)
正确的关注。
此外,您忘记了内存重新排序和缓存的可能性。例如,即使其他线程设置为top
,您的线程仍可能会看到NULL
的旧值。
答案 3 :(得分:0)
无锁数据结构的主题是Dobb博士的一个时尚主题,而它仍以纸质形式发布。仍然可以找到这些文章here。
答案 4 :(得分:0)
我看不出这是如何起作用的。我看到的一个问题是,当您在top
中取消引用top->next
时,该内存位置可能不再有效。某些其他线程可能已弹出堆栈并删除或以其他方式修改该项目。