好的,最近我一直在考虑线程安全问题,我想知道为什么链接列表或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
作为最后一个元素。
此方案中线程的问题在哪里?作家和读者不应该有任何问题,因为他们从不互相交流。它不像使用数组或向量(我很清楚它为什么会导致问题)。
答案 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);
写的值。
或其他可能出错的地方。
当另一个线程正在读取或写入相同的数据位置时,从一个线程写入数据位置是数据竞争,并且未定义具有数据争用的程序的行为。在实践中,这意味着它可以正常运行,直到您为最重要的客户演示程序,然后它会崩溃。