为什么链表不是线程安全的?

时间:2012-10-20 17:25:59

标签: c++ multithreading

好的,最近我一直在考虑线程安全问题,我想知道为什么链接列表或deques不是线程安全的。

假设我们有一个像这样的简单链表类:

class myLinkedList {
private:
    myLinkedList* next;
    int m_value;

public:
    myLinkedList() { next = NULL; m_value = 0; }
    void setValue(int value) { m_value = value; }
    int getValue() { return m_value; }
    void addNext(int value) { next = new myLinkedList; next->setValue(value); }
    myLinkedList* getNext() { return next; }
};

现在我只想在最后添加新元素并删除元素(首先读取它们然后删除它们)。我基本上只得到第一个next的地址,删除第一个元素并记住next作为我的新第一个元素。为了添加新元素,我只记得最后一个元素,当我添加新元素时,我只需设置一个新的next并记住我的新next作为最后一个元素。

此方案中线程的问题在哪里?作家和读者不应该有任何问题,因为他们从不互相交流。它不像使用数组或向量(我很清楚它为什么会导致问题)。

4 个答案:

答案 0 :(得分:5)

您的问题的评论是正确的,您的实施将无效。但是,要回答实际问题,您的代码中存在争用条件:

void addNext(int value) { next = new myLinkedList; next->setValue(value) }

想象一下线程A执行:

next = new myLinkedList;

现在线程A被抢占,线程B也执行相同的指令。这意味着next现在并没有指向线程A想要指向的位置,而是指向线程B设置它的位置。线程B继续执行:

next->setValue(value)

在那之后不久(或者甚至在同一时间),线程A也执行上述操作。

你能看到问题吗?主题A在B&#39 {s} next->setValue()上调用next,而{'} next丢失。

答案 1 :(得分:1)

当您在多个线程中共享容器(std::list或其他任何东西)的类实例时,您需要使用互斥锁或类似机制来保护并发访问以获取线程保存行为。

<强>更新

为您显示的构造

 void setValue(int value) { m_value = value; }
 int getValue() { return m_value; }
 void addNext(int value) { next = new myLinkedList; next->setValue(value); }

不是原子操作。因此,只要它们不在受保护的上下文中使用,它们就不是线程安全的。对于像std::list这样的STL容器也是如此。

答案 2 :(得分:1)

以下是转换为最基本的锻炼计划的问题中给出的代码:

class myLinkedList {
private:
    myLinkedList* next;
    int m_value;

public:
    myLinkedList() : next(0), m_value(0) { }
    int  getValue() const { return m_value; }
    void setValue(int value) { m_value = value; }
    void addNext(int value) { next = new myLinkedList; next->setValue(value); }
    const myLinkedList *getNext() const { return next; }
};

#include <iostream>

static void print_list(const myLinkedList *rover)
{
    std::cout << "List:";
    while (rover != 0)
    {
        std::cout << " " << rover->getValue();
        rover = rover->getNext();
    }
    std::cout << std::endl;
}

int main()
{
    myLinkedList mine;
    print_list(&mine);
    mine.addNext(13);
    print_list(&mine);
    mine.addNext(14);
    print_list(&mine);
    mine.setValue(3);
    print_list(&mine);
    mine.addNext(15);
    print_list(&mine);
}

该程序的输出是:

List: 0
List: 0 13
List: 0 14
List: 3 14
List: 3 15

如您所见,它不是正常的链表;它是最多两个项目的列表。在ll下运行程序(名为valgrind以获取链接列表),我得到:

==31288== Memcheck, a memory error detector
==31288== Copyright (C) 2002-2011, and GNU GPL'd, by Julian Seward et al.
==31288== Using Valgrind-3.7.0 and LibVEX; rerun with -h for copyright info
==31288== Command: ll
==31288== 
List: 0
List: 0 13
List: 0 14
List: 3 14
List: 3 15
==31288== 
==31288== HEAP SUMMARY:
==31288==     in use at exit: 6,239 bytes in 36 blocks
==31288==   total heap usage: 36 allocs, 0 frees, 6,239 bytes allocated
==31288== 
==31288== LEAK SUMMARY:
==31288==    definitely lost: 48 bytes in 3 blocks
==31288==    indirectly lost: 0 bytes in 0 blocks
==31288==      possibly lost: 0 bytes in 0 blocks
==31288==    still reachable: 6,191 bytes in 33 blocks
==31288==         suppressed: 0 bytes in 0 blocks
==31288== Rerun with --leak-check=full to see details of leaked memory
==31288== 
==31288== For counts of detected and suppressed errors, rerun with: -v
==31288== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 1 from 1)

如果代码是一个有效的链接列表实现,那么addNext()中就会出现一些时序问题。

基本上,您使用new创建一个节点,然后需要将其挂钩到列表中。如果另一个线程试图同时执行此操作,则您有一个可能导致结构不一致的时序窗口。为了线程安全,您需要在修改列表时确保互斥。

答案 3 :(得分:0)

有一些线程安全问题似乎很简单

next = new myLinkedList;

根据硬件的不同,next的分配可能会被线程切换破坏;也就是说,可能会写入部分值,然后处理器会在写入其余值之前更改为其他线程。然后另一个线程会看到一个垃圾值。

使用多个处理器时,还有另一个问题,即分配给next的值会写入执行存储的处理器的本地缓存。其他处理器有自己的缓存,因此可能看不到新值。或者他们可能会看到它,但看不到调用new myLinkedList所写的值,或者next->setValue(value);写的值。

或其他可能出错的地方。

当另一个线程正在读取或写入相同的数据位置时,从一个线程写入数据位置是数据竞争,并且未定义具有数据争用的程序的行为。在实践中,这意味着它可以正常运行,直到您为最重要的客户演示程序,然后它会崩溃。