非阻塞堆栈弹出

时间:2010-11-14 07:01:36

标签: c++ multithreading nonblocking

考虑以下使用非阻塞语义来弹出堆栈的代码:

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问题。

5 个答案:

答案 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,所以逻辑是:

  • 如果top为null,则队列为空
  • 如果top是sentinel,那么其他东西正在从堆栈中拉出来,去做其他有用的事情。
  • 如果top不是,请将sentinel放在top中,从result中获取值,put result - &gt;接下来在顶部,删除结果。

这是否仍然算作等待†取决于你是否能在中间状态下找到有用的东西。

有很多论文要比使用哨兵更有效地阅读 - 实际上你是用一个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时,该内存位置可能不再有效。某些其他线程可能已弹出堆栈并删除或以其他方式修改该项目。