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

C++ 我可以在等待/信号量中切换测试和修改部分吗?,c++,operating-system,semaphore,C++,Operating System,Semaphore,经典的none busy waiting版本的wait()和signal()信号量实现如下。在这个版本中,值可以是负数 //primitive wait(semaphore* S) { S->value--; if (S->value < 0) { add this process to S->list; block(); } } //primitive signal(semaphore* S) {

经典的
none busy waiting
版本的
wait()
signal()
信号量实现如下。在这个版本中,
值可以是负数

//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);
    }
}
//原语
等待(信号量*S)
{
S->值--;
如果(S->值<0)
{
将此流程添加到S->list中;
block();
}
}
//原始的
信号(信号量*S)
{
S->value++;
如果->值列表;
唤醒(P);
}
}
问题:以下版本也正确吗?这里我首先测试并修改值。如果您能向我展示一个不起作用的场景,那就太好了

//primitive wait().
//If (S->value > 0), the whole function is atomic
//otherise, only if(){} section is atomic
wait(semaphore* S)
{
    if (S->value <= 0)
    {
        add this process to S->list;
        block();
    }
    // here I decrement the value after the previous test and possible blocking
    S->value--;
}

//similar to wait()
signal(semaphore* S)
{
    if (S->list is not empty)
    {
        remove a process P from S->list;
        wakeup(P);
    }
    // here I increment the value after the previous test and possible waking up
    S->value++;
}
//原语等待()。
//如果(S->value>0),则整个函数是原子函数
//否则,仅当(){}节是原子的
等待(信号量*S)
{
如果->值列表;
block();
}
//在这里,我在上一次测试和可能的阻塞之后减小该值
S->值--;
}
//与wait()类似
信号(信号量*S)
{
如果->列表不为空
{
从S->list中删除流程P;
唤醒(P);
}
//在这里,我在上一次测试和可能的唤醒之后增加值
S->value++;
}
编辑


我的动机是想弄清楚我是否可以使用后一个版本来实现互斥、无死锁、无饥饿。

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

  • 线程A:if(S->Value<0)//Value=1
  • 线程B:if(S->Value<0)//Value=1
  • 线程A:S->Value--;//Value=0
  • 线程B:S->Value--;//Value=-1
两个线程都获得了count=1信号量。Oops。请注意,即使它们是不可抢占的(见下文),也存在另一个问题,但为了完整性,这里讨论了原子性以及真正的锁定协议是如何工作的

在使用这样的协议时,非常重要的一点是准确确定您使用的原子原语。原子原语似乎是即时执行的,而不会与任何其他操作交错。您不能只取一个大函数并将其称为原子函数;您必须以某种方式使其原子化,使用其他原子基元

大多数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)-将进程状态设置为睡眠,并将我们添加到等待队列,但继续执行(原子)
  • schedule()-切换线程。如果处于睡眠状态,则在唤醒(阻塞)之前不会再次运行
  • remove_waitqueue(waitqueue)-将进程从等待队列中移除,然后将状态设置为唤醒(如果尚未唤醒)(原子状态)
  • memory_barrier()-确保在此点之前的所有逻辑读/写操作实际上都是在此点之前执行的,避免了严重的内存排序问题(我们假设所有其他原子原语都有一个可用内存屏障,尽管这并不总是正确的)(CPU/编译器原语)
下面是一个典型的信号量获取例程的外观。它比您的示例要复杂一些,因为我已经明确确定了我使用的原子操作:

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
        }
    }
}
您会注意到,在实际的信号量下降函数中,对非零的
pSem->count
的实际测试是由
cmpxchg
完成的。您不能信任任何其他读取;读取值后,该值可以立即更改。我们无法将值检查和值修改分开

这里的
spec\u计数
是推测性的。这很重要。我基本上是在猜测计数会是什么。这是一个很好的猜测,但这是一个猜测。
cmpxchg
如果我的猜测是错误的,那么例程将失败,在这一点上必须循环并重试。如果我猜到0,那么我要么会被唤醒(当我在等待队列中时,它不再是零),或者我会注意到在计划测试中它不再是零

您还应该注意,没有可能的方法使包含阻塞操作的函数成为原子函数。这是毫无意义的。根据定义,原子函数似乎是瞬时执行的,而不是与任何其他函数交错。但是根据定义,阻塞函数等待其他事件发生。这是不一致的。Likewise,在你的例子中,没有原子操作可以在阻塞操作中被“拆分”

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

  • 线程A:if(S->Value<0)//Value=0
  • 线程A:块
  • 线程B:if(S->Value<0)//Value=0
  • 线程B:块
  • 线程C:S->Value++//Value=1
  • 线程C:唤醒(A)
  • (线程C再次调用signal())
  • 线程C:S->Value++//Value=2
  • 线程C:唤醒(B)
  • (线程C调用wait())
  • 线程C:if(S->Value--//Value=1
  • //A和B被吵醒了
  • 线程A:S->Value-->/Value=0
  • 线程B:S->Value-->/Value=-1
您可能会使用一个循环来重新检查S->value来解决这个问题,再次假设您在单处理器机器上,并且您的信号量代码是可抢占的。不幸的是,这些假设在所有桌面操作系统上都是错误的:)

有关真正的锁定协议如何工作的更多讨论,您可能会对“

您的m是什么”一文感兴趣