我可以在等待/信号信号量中切换测试和修改部分吗?
wait()
和 signal()
信号量的经典 none-busy-waiting
版本实现如下。在此版本中,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);
}
}
问题:下面的版本也正确吗?这里我先测试一下,修改一下值。如果您能给我展示一个它不起作用的场景,那就太好了。
//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++;
}
编辑:
我的动机是想弄清楚我是否可以使用后一个版本来实现互斥,并且没有死锁,没有饥饿。
The classic none-busy-waiting
version of wait()
and signal()
semaphore are implemented as below. In this verson, value
can be negative.
//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);
}
}
Question: Is the following version also correct? Here I test first and modify the value. It's great if you can show me a scenario where it doesn't work.
//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++;
}
Edit:
My motivation is to figure out whether I can use this latter version to achieve mutual exclusion, and no deadlock, no starvation.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
您的修改版本引入了竞争条件:
两个线程都获取了 count=1 信号量。哎呀。请注意,即使它们是不可抢占的(见下文),也存在另一个问题,但为了完整性,这里讨论原子性以及真正锁定协议的工作原理。
当使用这样的协议时,确定您正在使用的原子原语非常重要。原子原语看起来是即时执行的,而不与任何其他操作交织在一起。你不能只把一个大函数称为原子函数,而是将其称为原子函数。你必须使用其他原子基元以某种方式使其原子化。
大多数 CPU 都提供称为“原子比较和交换”的原语。从这里开始我将其缩写为 cmpxchg。语义如下:
cmpxchg
未使用此代码实现。它位于 CPU 硬件中,但行为有点像这样,只是原子地。现在,让我们添加一些额外的有用函数(由其他原语构建):
这是典型的信号量获取例程的外观。它比您的示例复杂一点,因为我已经明确确定了我正在使用的原子操作:
您会注意到非零
pSem->count
的实际测试,在现实世界的 semaphore_down 函数中,是由 cmpxchg 完成的。你不能相信任何其他读物;读取该值后,该值可能会立即发生变化。我们根本无法将值检查和值修改分开。这里的
spec_count
是推测。这很重要。我基本上是在猜测计数会是多少。这是一个很好的猜测,但它只是一个猜测。如果我的猜测错误,cmpxchg
将失败,此时例程必须循环并重试。如果我猜为 0,那么我要么会被叫醒(因为当我在等待队列上时它不再为零),要么我会注意到它在计划测试中不再为零。您还应该注意,没有可能的方法使包含阻塞操作的函数成为原子的。这是荒谬的。根据定义,原子函数似乎是立即执行的,不与其他任何东西交错。但根据定义,阻塞函数会等待其他事情发生。这是不一致的。同样,任何原子操作都不能在阻塞操作中“拆分”,如您的示例所示。
现在,您可以通过声明函数不可抢占来消除很多这种复杂性。通过使用锁或其他方法,您只需确保信号量代码中一次只有一个线程运行(当然不包括阻塞)。但问题仍然存在。从值 0 开始,其中 C 已将信号量取下两次,然后:
您可能可以通过循环重新检查 S->value 来修复此问题 - 假设您在单处理器计算机上并且您的信号量代码是可抢占的。不幸的是,这些假设在所有桌面操作系统上都是错误的:)
有关真实锁定协议如何工作的更多讨论,您可能会对论文“Fuss、Futexes 和 Furwocks:Linux 中的快速用户级锁定"
Your modified version introduces a race condition:
Both threads have acquired a count=1 semaphore. Oops. Note that there's another problem even if they're non-preemptible (see below), but for completeness, here's a discussion on atomicity and how real locking protocols work.
When working with protocols like this, it's very important to nail down exactly what atomic primitives you are using. Atomic primitives are such that they seem to execute instantaneously, without being interleaved with any other operations. You cannot just take a big function and call it atomic; you have to make it atomic somehow, using other atomic primitives.
Most CPUs offer a primitive called 'atomic compare and exchange'. I'll abbreviate it cmpxchg from here on. The semantics are like so:
cmpxchg
is not implemented with this code. It is in the CPU hardware, but behaves a bit like this, only atomically.Now, let's add to this some additional helpful functions (built out of other primitives):
Here's how a typical semaphore acquisition routine will look. It's a bit more complex than your example, because I've explicitly nailed down what atomic operations I'm using:
You'll note that the actual test for a non-zero
pSem->count
, in a real-world semaphore_down function, is accomplished bycmpxchg
. You can't trust any other read; the value can change an instant after you read the value. We simply can't separate the value check and the value modification.The
spec_count
here is speculative. This is important. I'm essentially making a guess at what the count will be. It's a pretty good guess, but it's a guess.cmpxchg
will fail if my guess is wrong, at which point the routine has to loop and try again. If I guess 0, then I will either be woken up (as it ceases to be zero while I'm on the waitqueue), or I will notice it's not zero anymore in the schedule test.You should also note that there is no possible way to make a function that contains a blocking operation atomic. It's nonsensical. Atomic functions, by definition, appear to execute instantaneously, not interleaved with anything else whatsoever. But a blocking function, by definition, waits for something else to happen. This is inconsistent. Likewise, no atomic operation can be 'split up' across a blocking operation, which it is in your example.
Now, you could do away with a lot of this complexity by declaring the function non-preemptable. By using locks or other methods, you simply ensure only one thread is ever running (not including blocking of course) in the semaphore code at a time. But a problem still remains then. Start with a value of 0, where C has taken the semaphore down twice, then:
You could probably fix this with a loop to recheck S->value - again, assuming you are on a single processor machine and your semaphore code is preemptable. Unfortunately, these assumptions are false on all desktop OSes :)
For more discussion on how real locking protocols work, you might be interested in the paper "Fuss, Futexes and Furwocks: Fast Userlevel Locking in Linux"