原始同步原语 - 安全吗?

时间:2009-04-23 18:15:46

标签: multithreading synchronization thread-safety locking

在受约束的设备上,我经常发现自己在两个线程之间“假装”锁定了2个bool。每个只由一个线程读取,只由另一个线程写入。这就是我的意思:

bool quitted = false, paused = false;
bool should_quit = false, should_pause = false;

void downloader_thread() {
    quitted = false;
    while(!should_quit) {
        fill_buffer(bfr);
        if(should_pause) {
            is_paused = true;
            while(should_pause) sleep(50);
            is_paused = false;
        }
    }
    quitted = true;
}

void ui_thread() {
    // new Thread(downloader_thread).start();
    // ...
    should_pause = true;
    while(!is_paused) sleep(50);
        // resize buffer or something else non-thread-safe
    should_pause = false;
}

当然在PC上我不会这样做,但在受限设备上,似乎读取bool值比获取锁定要快得多。当然,当需要更改缓冲区时,我需要权衡较慢的恢复(参见“sleep(50)”)。

问题 - 它是完全线程安全的吗?或者在伪造这样的锁时我需要注意隐藏的陷阱吗?或者我不应该这样做?

5 个答案:

答案 0 :(得分:6)

使用bool值在线程之间进行通信可以按预期工作,但确实有两个隐藏的陷阱,如this blog post by Vitaliy Liptchinsky中所述:

缓存一致性

CPU并不总是从RAM中获取内存值。芯片上的快速内存缓存是CPU设计人员用来解决Von Neumann bottleneck的技巧之一。在某些多CPU或多核架构(如英特尔Itanium)上,这些CPU缓存不会被共享或自动保持同步。换句话说,如果它们在不同的CPU上运行,那么你的线程可能会看到相同内存地址的不同值。

为避免这种情况,您需要将变量声明为易变C++C#java),或执行explicit volatile read/writes,或者利用锁定机制。

编译器优化

如果涉及多个线程,编译器或JITter可能会执行不安全的优化。有关示例,请参阅链接的博客文章。同样,您必须使用volatile关键字或其他机制来通知编译器。

答案 1 :(得分:5)

除非您详细了解设备的内存架构以及编译器生成的代码,否则此代码并不安全。

仅仅因为看起来它会起作用,并不意味着它会。 “约束”设备,如无约束类型,正变得越来越强大。例如,我不打赌在手机中找到双核CPU。这意味着我不会打赌上面的代码会起作用。

答案 2 :(得分:0)

关于睡眠呼叫,您可以随时执行睡眠(0)或暂停线程的等效呼叫,让下一个回合。

关于其余部分,如果您知道设备的实施细节,这是线程安全的。

答案 3 :(得分:0)

回答问题。

这完全是线程安全吗?我会回答不,这不是线程安全的,我根本就不会这样做。在不知道我们的设备和编译器的细节的情况下,如果这是C ++,编译器可以根据需要自由地重新排序和优化。例如你写道:

is_paused = true;            
while(should_pause) sleep(50);            
is_paused = false;

但编译器可能会选择将其重新排序为:

sleep(50);
is_paused = false;

这可能不会像其他人所说的那样对单个核心设备起作用。

您可以尝试在UI线程上做更少而不是在处理UI消息的过程中产生更好的效果。如果您认为您在UI线程上花费了太多时间,那么就找到一种方法来干净地退出并注册异步回调。

如果你在UI线程上调用sleep(或尝试获取一个锁或做任何可能阻塞的事件),你就会打开门以便挂起并出现故障UI。 50毫秒的睡眠足以让用户注意到。如果您尝试获取锁定或执行任何其他阻塞操作(如I / O),您需要处理等待不确定的时间来获取I / O的现实,这些I / O往往会从故障转换为挂起。

答案 4 :(得分:0)

此代码几乎在所有情况下都不安全。在多核处理器上,核心之间不会有缓存一致性,因为bool读取和写入不是原子操作。这意味着如果尚未刷新上一次写入的缓存,则不保证每个核心在缓存中具有相同的值,甚至从内存中获得相同的值。

但是,即使在资源受限的单核设备上,这也不安全,因为您无法控制调度程序。这是一个例子,为了简单起见,我假装这些是设备上唯一的两个线程。

当ui_thread运行时,以下代码行可以在同一时间片中运行。

// new Thread(downloader_thread).start();
// ...
should_pause = true;

downloader_thread接下来运行,并在其时间片中执行以下行:

quitted = false;
while(!should_quit)
{
    fill_buffer(bfr);

调度程序在fill_buffer返回之前预先设置downloader_thread,然后激活运行的ui_thread。

while(!is_paused) sleep(50);
// resize buffer or something else non-thread-safe
should_pause = false;

调整大小缓冲区操作是在downloader_thread正在填充缓冲区的过程中完成的。这意味着缓冲区已损坏,您很快就会崩溃。它不会每次都发生,但是在将is_paused设置为true之前填充缓冲区这一事实使得它更有可能发生,但即使你在downloader_thread上切换了这两个操作的顺序,你仍然会遇到竞争条件,但你可能会死锁,而不是破坏缓冲区。

顺便说一句,这是一种螺旋锁,它只是不起作用。 Spinlock的等待时间不是很长,可能会跨越许多时间片导致处理器旋转。你的implmentation做睡眠有点好,但调度程序仍然必须运行你的线程和线程上下文切换并不便宜。如果您正在等待关键部分或信号量,则在资源变为空闲之前,调度程序不会再次激活您的线程。

您可能能够在某个特定平台/架构上以某种形式摆脱这种情况,但很容易犯一个很难追踪的错误。