我可以在等待/信号信号量中切换测试和修改部分吗?

时间:2011-04-30 03:36:57

标签: c++ operating-system semaphore

none-busy-waiting和[{1}}信号量的经典wait()版本实现如下。在本节中,signal()可能是否定的。

value

问题:以下版本是否也正确?在这里,我首先测试并修改值。如果你能告诉我一个不起作用的场景,那就太好了。

//primitive
wait(semaphore* S)
{
    S->value--;
    if (S->value < 0)
    {
        add this process to S->list;
        block();
    }
}

//primitive
signal(semaphore* S)
{
    S->value++;
    if (S->value <= 0)
    {
        remove a process P from S->list;
        wakeup(P);
    }
}

修改

我的动机是弄清楚我是否可以使用后一种版本来实现互斥,没有死锁,没有饥饿。

1 个答案:

答案 0 :(得分:1)

您的修改版本引入了竞争条件:

  • 线程A:if(S->值<0)//值= 1
  • 线程B:if(S-> Value&lt; 0)// Value = 1
  • 主题A:S-&gt;值 - ; //值= 0
  • 线程B:S-&gt;值 - ; //值= -1

两个线程都获得了count = 1的信号量。哎呀。请注意,即使它们不可抢占也存在另一个问题(见下文),但为了完整性,这里讨论了原子性以及真正的锁定协议的工作原理。

使用这样的协议时,确切地确定使用的原子基元非常重要。原子基元是这样的,它们似乎是即时执行的,而不与任何其他操作交错。你不能只是把一个大功能称为原子;你必须使用其他原子基元以某种方式使原子。

大多数CPU提供称为“原子比较和交换”的原语。我将从这里缩写为cmpxchg。语义就像这样:

bool cmpxchg(long *ptr, long old, long new) {
    if (*ptr == old) {
        *ptr = new;
        return true;
    } else {
        return false;
    }
}
使用此代码

cmpxchg未实现 。它在CPU硬件中,但行为有点像这样,只是原子的。

现在,让我们添加一些额外的有用功能(由其他原语构建):

  • add_waitqueue(waitqueue) - 将我们的进程状态设置为休眠并将我们添加到等待队列,但继续执行(ATOMIC)
  • schedule() - 切换线程。如果我们处于睡眠状态,我们不会再醒来直到被唤醒(阻塞)
  • remove_waitqueue(waitqueue) - 从等待队列中删除我们的进程,然后将我们的状态设置为唤醒(如果它还没有)(ATOMIC)
  • memory_barrier() - 确保在此点之前逻辑上进行任何读/写操作实际上在此点之前执行,避免讨厌的内存排序问题(我们假设所有其他原子基元都带有空闲内存障碍,虽然并非总是如此)(CPU / COMPILER PRIMITIVE)

以下是典型的信号量采集程序的外观。它比你的例子复杂一点,因为我已经明确地确定了我正在使用的原子操作:

void sem_down(sem *pSem)
{
    while (1) {
        long spec_count = pSem->count;
        read_memory_barrier(); // make sure spec_count doesn't start changing on us! pSem->count may keep changing though
        if (spec_count > 0)
        {
            if (cmpxchg(&pSem->count, spec_count, spec_count - 1)) // ATOMIC
                return; // got the semaphore without blocking
            else
                continue; // count is stale, try again
        } else { // semaphore count is zero
            add_waitqueue(pSem->wqueue); // ATOMIC
            // recheck the semaphore count, now that we're in the waitqueue - it may have changed
            if (pSem->count == 0) schedule(); // NOT ATOMIC
            remove_waitqueue(pSem->wqueue); // ATOMIC
            // loop around again to try to acquire the semaphore
        }
    }
}

您会注意到,在真实世界的semaphore_down函数中,非零pSem->count的实际测试是由cmpxchg完成的。你不能相信任何其他阅读;读取值后,该值可能会立即更改。我们根本无法将值检查和值修改分开。

这里的spec_count推测。这个很重要。我基本上猜测计数是多少。这是一个非常好的猜测,但它是一个猜测。如果我的猜测错误,cmpxchg将失败,此时例程必须循环并再次尝试。如果我猜0,那么我会被唤醒(因为它在等待时我不再为零),或者我会注意到它在计划测试中不再为零。

您还应该注意,没有办法制作包含阻塞操作原子的函数。这是荒谬的。根据定义,原子功能似乎是即时执行的,而不是与任何其他任何东西交错。但是根据定义,阻塞函数会等待其他事情发生。这是不一致的。同样,没有原子操作可以在阻塞操作中“拆分”,就像你的例子一样。

现在,您可以通过声明函数不可抢占来消除这种复杂性。通过使用锁或其他方法,您只需确保一次只能在信号量代码中运行一个线程(当然不包括阻塞)。但问题仍然存在。从值0开始,其中C将信号量下调两次,然后:

  • 线程A:if(S->值<0)//值= 0
  • 主题A:阻止....
  • 线程B:if(S-> Value&lt; 0)// Value = 0
  • 线程B:阻止....
  • 主题C:S->值++ //值= 1
  • 线程C:唤醒(A)
  • (线程C再次调用signal())
  • 主题C:S->值++ //值= 2
  • 线程C:唤醒(B)
  • (线程C调用wait())
  • 线程C:if(S->值<= 0)//值= 2
  • 主题C:S-&gt; Value-- // Value = 1
  • // A和B已被唤醒
  • 主题A:S-&gt; Value-- // Value = 0
  • 主题B:S-&gt; Value-- // Value = -1

您可以通过循环重新检查S-&gt;值来解决此问题 - 再次假设您在单处理器计算机上并且您的信号量代码是可抢占的。不幸的是,这些假设在所有桌面操作系统上都是错误的:)

有关真正的锁定协议如何工作的更多讨论,您可能会对“Fuss, Futexes and Furwocks: Fast Userlevel Locking in Linux”文章感兴趣