4

在受限设备上,我经常发现自己在 2 个线程之间用 2 个布尔值“伪造”锁。每个线程只能由一个线程读取,并且只能由另一个线程写入。这就是我的意思:

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)当需要更改缓冲区时,我会以较慢的恢复速度(参见“”)进行权衡。

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

4

5 回答 5

6

使用布尔值在线程之间进行通信可以按您的意愿工作,但确实有两个隐藏的陷阱,正如Vitaliy Liptchinsky 的这篇博文中所解释的那样:

缓存一致性

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

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

编译器优化

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

于 2009-04-23T23:03:53.380 回答
5

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

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

于 2009-04-23T18:20:19.940 回答
0

关于 sleep 调用,您总是可以只执行 sleep(0) 或等效的调用来暂停您的线程,让下一个线程转一圈。

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

于 2009-04-23T18:22:23.760 回答
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 往往会从故障转换为挂起。

于 2009-04-23T22:28:18.037 回答
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 上切换了这两个操作的顺序,您仍然会有竞争条件,但您可能会死锁而不是破坏缓冲区。

顺便说一句,这是一种自旋锁,它只是不起作用。自旋锁不太适合可能跨越许多时间片的等待时间导致处理器旋转。您的实现确实 sleep 这有点好,但是调度程序仍然必须运行您的线程,并且线程上下文切换并不便宜。如果您正在等待关键部分或信号量,则调度程序不会再次激活您的线程,直到资源变得可用。

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

于 2009-04-24T07:47:26.460 回答