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是什么”一文感兴趣