“忙等待”和“休眠”之间的权衡是什么?

31

这是我之前问题的延伸。

Unix/Linux套接字中的阻塞模式如何工作?

根据我从互联网上了解到的,所有调用阻塞方法的进程都会被挂起,直到调度程序找到解除阻塞的原因。这些原因可能因缓冲区为空或缓冲区已满等条件而异。

但是这种方法能否有效地应用于实时应用程序,例如硬/软实时应用程序?由于进程在解除阻塞条件成立时并不会立即解除阻塞,而是在调度程序给他分配CPU时间片并且解除阻塞条件成立时才会解除阻塞。

如果您需要一个响应迅速的解决方案,我不认为“自旋锁”或“忙等待”是正确的方法,过多的CPU时间片将被浪费,整个系统将变得无法响应或反应效果较差。

请问有人能够澄清这些相互矛盾的想法吗?

6个回答

50

通常/首选的做法是在调度程序唤醒您之前进入睡眠状态。

自旋(不使用睡眠的替代方法)较少使用,会产生以下影响:

  • 使CPU保持繁忙状态,防止其他线程使用CPU(直到/除非旋转线程完成其时间片并被强制抢占)

  • 可以在你等待的事情发生的那一刻立即停止自旋(因为你在不断地检查事件,且无需花费等待唤醒所需的时间,因为你已经处于唤醒状态)

  • 不会调用进入睡眠状态和唤醒的CPU指令

如果延迟时间很短(例如仅需要执行100个CPU指令的时间),则自旋可能比进入睡眠状态更高效(总CPU使用量更少)。


4
自旋锁会消耗CPU和轮询资源路径,一直浪费资源,直到期望的事件发生。 阻塞操作最重要的区别在于不占用CPU和相关资源路径,并且在期望的资源上安装了某种形式的等待
在多任务或多线程/处理器环境(长期以来通常情况下),当期望的事件未到达时,占用CPU和资源访问路径会导致可怕的处理能力和时间浪费。
当我们拥有一个超线程系统(就像我认为您在问题中提到的那样),需要注意的是CPU线程切片的粒度非常高。我也会冒着风险观察到所有事件 - 您倾向于被阻止的事件 - 都需要足够的时间才能出现,在解除阻塞之前需要等待额外的小时间片。
我认为J-16的观点是针对处于阻塞状态的睡眠(阻塞)线程未使用其代码和数据空间的情况。这可能会使系统放弃资源(如数据/代码缓存),然后需要在释放块时重新填充。因此,在特定条件下,阻塞可能会导致更多的资源浪费。
这也是一个有效的注意事项,应在设计和实施中进行检查。
但是,在大多数情况下,阻塞通常比自旋锁更好。

好的,我想我会发表一个答案,看看人们是否同意,或者我错了。不过还是谢谢你。是的,j16带来了不同的视角,这很好。感谢尼克的强调。 :) - Vivek Sharma

2
如果在您的应用程序使用情况下,上下文切换比消耗一些CPU周期更昂贵,因为您的条件将在短时间内得到满足,那么忙等待可能适合您。
否则,您可以通过休眠或使用cond_wait()来强制释放CPU。
我能想到的另一种强制上下文切换的场景如下:
while(condition)
    sleep(0);

0

首先,您有一个误解:

阻塞调用不是“忙等待”或“自旋锁”。阻塞调用是可休眠的——这意味着 CPU 会处理其他任务,没有浪费 CPU。

关于您对阻塞调用的问题

阻塞调用更容易——它们易于理解、易于开发、易于调试。

但它们会占用资源。如果您不使用线程,它将阻塞其他客户端;如果您使用线程,每个线程都会占用内存和其他系统资源。即使您有足够的内存,切换线程也会使缓存变冷并降低性能。

这是一种权衡——更快的开发和可维护性?还是可扩展性。


1
@j-16,那正是我所说的,“阻塞调用是可休眠的”,请再次仔细阅读问题。 - Vivek Sharma
如果阻塞调用正在休眠,它们不会占用任何CPU资源,那么它们怎么可能成为资源猪呢? - Vivek Sharma
@Vivek:这里的“resource”主要指内存。 - J-16 SDiZ
@j-16,这里的每个人都在谈论CPU周期,因此我认为资源(虽然主观)显然是CPU。而且,根据我的理解,线程切换与进程的上下文切换是不同的。这取决于线程如何绑定到内核的LWP,但这是另一个实现特定的话题。 哦,好的,你的回答中提到的资源是指内存。 - Vivek Sharma
@vivek:切换线程并不会切换整个内存空间,但会切换寄存器和堆栈--这就是上下文切换。即使忽略内存使用(可能相当大),更改堆栈和重新填充缓存也需要 CPU 时间成本。 - J-16 SDiZ

0

我会尽量简明扼要地说明问题,因为其他答案已经提供了足够的解释。通过学习这些答案,我认为可以得到一个完整的图片。

在我看来,权衡应该在系统的响应能力和吞吐量之间进行。

响应能力 - 可以从两个角度考虑

  • 整体系统响应能力,以及
  • 特定或每个进程的响应能力

我认为对于系统的响应能力,阻塞调用是最好的选择。因为当阻塞调用处于阻塞状态时,它将把CPU交给就绪队列中的其他进程。

当然,对于特定进程或每个进程的响应能力,我们应该考虑使用忙等待/自旋锁模型。

现在,为了提高整个系统的响应能力,我们不能减少调度程序的时间片(细粒度),因为这会浪费太多的CPU资源在上下文切换中。因此,系统的吞吐量会急剧下降。当然,阻塞模型增加了系统的吞吐量,因为被阻塞的调用不会消耗CPU时间片,并将其分配给就绪队列中的其他/下一个进程。

我认为最好的方法是--设计一个以每个进程的响应性为重点的系统,而不影响整体响应性和吞吐量--通过实现基于优先级的调度程序,并考虑优先级反转问题,如果增加复杂性不会困扰您 :)


只想补充一点:希望您能正确地权衡自旋锁资源利用的低效性和阻塞模式(通常是硬件中的细粒度)调度时间片的有效延迟。 - nik
是的 Nik,你说得对,整个辩论围绕这种权衡展开。 - Vivek Sharma

-3

//改编自ASPI原始源代码...

    DWORD startStopUnit (HANDLE handle, BOOL bLoEj, BOOL bStart)
    {
       DWORD  dwStatus;
       HANDLE heventSRB;
       SRB_ExecSCSICmd s;





    //here
       heventSRB = CreateEvent (NULL, TRUE, FALSE, NULL);

       memset (&s, 0, sizeof (s));

       s.SRB_Cmd = SC_EXEC_SCSI_CMD;
       s.SRB_HaID = 0;
       s.SRB_Target = 0;
       s.SRB_Lun = 0;
       s.SRB_Flags = SRB_EVENT_NOTIFY;
       s.SRB_SenseLen = SENSE_LEN;
       s.SRB_CDBLen = 6;
       s.SRB_PostProc = (LPVOID) heventSRB;
       s.CDBByte[0] = 0x1B;
       s.CDBByte[4] |= bLoEj ? 0x02 : 0x00;
       s.CDBByte[4] |= bStart ? 0x01 : 0x00;

       ResetEvent (heventSRB);
       dwStatus = SPTISendASPI32Command (handle,(LPSRB) & s);
       if (dwStatus == SS_PENDING)
         {
//and here, don´t know a better way to wait for something to finish without processor cicles
        WaitForSingleObject (heventSRB, DEFWAITLEN);
         }
       CloseHandle (heventSRB);

       if (s.SRB_Status != SS_COMP)
         {
    printf("Erro\n");
        return SS_ERR;
         }

       printf("nao Erro\n");
       return s.SRB_Status;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接