【问题标题】:Is it necessary to defer deleting an object in the following multi-thread stack?下面的多线程堆栈中是否需要延迟删除对象?
【发布时间】:2021-02-24 02:49:02
【问题描述】:

C++ Concurrency in Action的代码清单 7.3 中的以下代码:

template<typename T> 
class lock_free_stack { 
private:
    struct node { 
        std::shared_ptr<T> data; 
        node* next; 
        node(T const& data_): data(std::make_shared<T>(data_)) {} 
    }; 
    std::atomic<node*> head;

public:
    void push(T const& data) { 
        node* const new_node = new node(data); 
        new_node->next = head.load(); 
        while(!head.compare_exchange_weak(new_node->next, new_node)); 
    } 
    std::shared_ptr<T> pop() { 
        node* old_head = head.load(); 
        while(old_head 
            && !head.compare_exchange_weak(old_head, old_head->next)); 
        
        auto res = old_head ? old_head->data : std::shared_ptr<T>();
        if (old_head)
            delete old_head;
        return res;
    }
};

唯一的修改是我在检索数据后立即删除了old_head。作者 Anthony Williams 在第 7.2.2 小节(第 214 页)中说,当多个线程同时调用 pop 时,此时删除 old_head 是不安全的。我想知道为什么会这样。

在我看来,while 循环while(old_head &amp;&amp; !head.compare_exchange_weak(old_head, old_head-&gt;next)); 确保不会有两个线程在while 循环之后old_head 指向同一个节点。换句话说,如果线程 A 和线程 B 都完成了 while 循环,则线程 A 中的 old_head 必须指向与线程 B 中的 old_head 所指向的节点不同的节点。如果线程 A 和线程 B 中的 old_heads 都指向同一个节点,则至少其中一个仍位于 while 循环之前的点。

假设线程 B 首先完成了 while 循环,而线程 A 仍然在 while 循环之前。假设线程 B 在线程 A 继续之前删除了节点。问题变成了这是否会导致线程 A 的未定义行为。在 while 循环的第一次迭代中,由于线程 B,线程 A 中的old_head 被删除,head 指向与old_head 不同的位置。因此,head.compare_exchange_weak(old_head, old_head-&gt;next) 会将head 加载到old_head 并返回false。因为old_next-&gt;next现在还没有使用,所以不会有任何问题。我使用以下代码验证这一点

struct Node {
    int v;
    Node* next;
};


int main() {
    std::atomic head = new Node();
    auto old_node = head.load();
    old_node->next = new Node();
    head.exchange(old_node->next);
    delete old_node;
    std::cout << bool(old_node) << '\n';    // print 1
    head.compare_exchange_weak(old_node, old_node->next);
    std::cout << old_node->v << '\n';       // print 0
}

在我看来,在检索到数据后立即删除old_head没有问题,因为即使另一个线程中有一个指针指向已删除的内存,只要我们不取消引用它仍然是安全的那个指针。我是对的,还是我误解了什么?

【问题讨论】:

  • 你真的只需要两个动作。推,弹出。
  • 代码不会像写的那样工作——当 compare_exchange_weak 失败时,它需要在 while 循环中再次调用 head.load()
  • 嗨@ChrisDodd。为什么需要在while循环中调用head.load()?当compare_exchange_weak失败时,不应该自动将old_head分配给head.load()吗?

标签: c++ multithreading


【解决方案1】:

删除old_head 的风险是第二个(或第三个,或...)线程仍可能处于等待更新head 的while 循环中。第一个线程可以删除old_head就在另一个线程读取old_head-&gt;next 作为对compare_exchange_weak 的调用的一部分。

【讨论】:

  • 如果第一个线程删除old_head,那么在第二个线程中,head被第一个线程更新,不再等于old_head。在这种情况下,compare_exchange_weak 失败,old_head 将重新分配给head。问题出在哪里?您可以在我的第二个代码块中看到,这不是问题。
  • @也许线程 2 读取/加载 old_head 的当前值(并获得与第一个线程相同的值)之间有一个非常小的窗口,第一个线程更新 head并删除old_head,然后第二个线程尝试读取old_head-&gt;next。这可能会访问已被销毁的对象。更新失败的事实无关紧要:导致未定义行为的是对已破坏对象的读取。 (诚​​然,这种情况非常罕见,即使读得不好,通常也不会崩溃。)
  • 嗨@1201ProgramAlarm。据我了解,如果第一个线程删除old_head,在线程2中,headold_head将不再指向相同的值。因此,compare_exchange_weak 会调用类似old_head=head.load() 的东西,它不涉及old_head-&gt;next 并且可以。在第二个代码块中,我在head.compare_exchange_weak(old_node, old_node-&gt;next) 之前删除了old_node 并且没有抛出错误。那么可能发生错误的窗口在哪里呢?你能说得更具体点吗?
  • @也许有一个小窗口之后调用head.load()获取存储在head中的值,但之前表达式@ 987654346@ 被评估。
  • 在这种情况下,不使用old_head != head.load()old_head-&gt;next。不是吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-05-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-05
  • 2014-01-19
  • 1970-01-01
相关资源
最近更新 更多