list :: empty()多线程行为?


9

我有一个列表,希望不同的线程从中获取元素。为了避免在列表为空时锁定保护该列表的互斥锁,请empty()在锁定之前进行检查。

如果对电话的呼叫在list::empty()100%的时间内都不正确,则可以。我只想避免崩溃或中断并发list::push()list::pop()调用。

我是否可以肯定地认为VC ++和Gnu GCC有时只会empty()出错并且没有比这更糟的了?

if(list.empty() == false){ // unprotected by mutex, okay if incorrect sometimes
    mutex.lock();
    if(list.empty() == false){ // check again while locked to be certain
         element = list.back();
         list.pop_back();
    }
    mutex.unlock();
}

1
不,你不能假设这一点。你可以使用像VC的并发容器concurrent_queue
帕纳约蒂斯Kanavos

2
@Fureeish这应该是一个答案。我还要补充一点,std::list::size它保证了恒定的时间复杂度,这基本上意味着大小(节点数)需要存储在单独的变量中;我们称之为size_std::list::empty那么可能会返回size_ == 0,并且的并发读取和写入size_将导致数据争用,因此UB。
丹尼尔·兰格

@DanielLangr如何测量“恒定时间”?是在单个函数调用上还是在整个程序上?
curiousguy19年

1
@curiousguy:DanielLangr确实通过“独立于列表节点的数量”回答了您的问题,这就是O(1)的确切定义,这意味着无论元素数量如何,每次调用都在不到一定的固定时间内执行。en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions其他选项(直到C ++ 11)将是线性= O(n),这意味着大小必须计算元素(链接列表),对于以下情况甚至更糟并发(比计数器上的非原子读取/写入更明显的数据争用)。
firda

1
@curiousguy:以您自己的dV为例,时间复杂度是相同的数学-极限。所有这些东西或者是递归定义的,或者是以“存在C使得f(N)<C每N个”的形式定义的-这就是O(1)的定义(对于给定的/每个HW,存在常数C这样在任何输入上算法的结束时间都小于C时间)。平均摊销意味着,这意味着某些输入可能需要更长的时间来处理(例如,需要重新哈希/重新分配),但平均而言仍保持不变(假设所有可能的输入)。
菲尔达

Answers:


10

如果对电话的呼叫在list::empty()100%的时间内都不正确,则可以。

不,这不好。如果在某种同步机制(锁定互斥锁)之外检查列表是否为空,则将发生数据争用。进行数据竞争意味着您具有未定义的行为。具有未定义的行为意味着我们不再能够对该程序进行推理,并且您获得的任何输出都是“正确的”。

如果您珍惜自己的理智,则可以在检查之前降低性能并锁定互斥锁。也就是说,该列表甚至可能不是您的正确容器。如果您可以让我们确切知道您在做什么,我们也许可以建议一个更好的容器。


个人观点,通话list::empty()是具有无关的读取动作race-condition
玉庆阮

3
@NgọcKhánhNguyễn如果他们将元素添加到列表中,那么在您同时写入和读取大小时,无疑会导致数据争用。
NathanOliver

6
@NgọcKhánhNguyễn这是错误的。比赛条件是read-writewrite-write。如果您不相信我,请阅读数据竞赛的标准部分
NathanOliver

1
@NgọcKhánhNguyễn:因为不能保证写操作和读取操作都是原子的,所以可以同时执行,因此读操作可能会完全出错(称为撕裂读操作)。想象一下,在小端8位MCU中将写0x00FF更改为0x0100,首先将低0xFF重写为0x00,现在读取正好为零,读取两个字节(写入线程已减慢或挂起),写入继续,将高字节更新为0x01,但是读取线程已经获得了错误的值(既不是0x00FF,也不是0x0100,而是意外的0x0000)。
firda

1
@NgọcKhánhNguyễn可能在某些体系结构上,但C ++虚拟机不提供此类保证。即使您的硬件做到了,编译器也可以以您永远不会看到的更改的方式优化代码,这是合法的,因为除非进行线程同步,否则它可以假定它仅运行一个线程并进行相应的优化。
NathanOliver

6

有一个读取和写入(最有可能的size成员std::list,如果我们假设它的命名一样,)不同步在reagard彼此。想象一个线程empty()(在您的外部if())调用,而另一个线程进入内部if()并执行pop_back()。然后,您正在读取可能正在修改的变量。这是未定义的行为。


2

举例说明事情可能会出错:

一个足够聪明的编译器可能会看到它mutex.lock()不可能更改list.empty()返回值,因此if完全跳过了内部检查,最终导致pop_back列表中的a 在第一个元素之后删除了最后一个元素if

为什么能做到这一点?中没有同步list.empty(),因此如果同时进行更改,则将构成数据竞争。该标准说程序不应该有数据争用,因此编译器会认为这是理所当然的(否则它几乎不能执行任何优化)。因此,它可以对未同步的情况采取单线程观点,list.empty()并得出结论,必须保持不变。

这只是可能破坏您的代码的几种优化(或硬件行为)之一。


当前的编译器甚至都不想优化a.load()+a.load()...
curiousguy

1
@curiousguy会如何优化?您在那里要求完全的顺序一致性,所以您会得到...
Max Langhof

@MaxLanghof您认为优化a.load()*2并不明显吗?甚至a.load(rel)+b.load(rel)-a.load(rel)没有优化。没有什么是。您为什么期望锁(本质上主要具有seq一致性)会得到更好的优化?
curiousguy

@curiousguy因为非原子访问(在锁之前和之后)和原子的内存顺序完全不同?我不希望“更多”地优化锁,我希望不同步的访问比顺序一致的访问更能被优化。我的观点与锁的存在无关。不,编译器不允许优化a.load() + a.load()2 * a.load()。如果您想了解更多,请随时提出一个问题。
马克斯·朗霍夫

@MaxLanghof我什至不知道你在说什么。锁基本上是顺序一致的。为什么实现会尝试对某些线程原语(锁)而不是其他某些原语(原子)进行优化?您是否希望围绕原子的使用来优化非原子访问?
curiousguy19年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.