“忙碌等待”与“睡眠”的权衡是啥?

Posted

技术标签:

【中文标题】“忙碌等待”与“睡眠”的权衡是啥?【英文标题】:What are trade offs for "busy wait" vs "sleep"?“忙碌等待”与“睡眠”的权衡是什么? 【发布时间】:2010-11-09 14:32:12 【问题描述】:

这是对我之前问题的扩展

How does blocking mode in unix/linux sockets works?

我现在从 Internet 收集到的信息,所有调用阻塞调用的进程都处于休眠状态,直到调度程序找到解除阻塞的原因。原因可能会有所不同,从缓冲区空到缓冲区满再到任何其他情况。

但是,这是否是一种有效的实时方式,比如说硬/固定实时应用程序?因为当解锁条件为真时,进程并没有被解锁,而是当调度器给他他的 CPU 片时,并且解锁条件都为真。

好像你想要一个响应式解决方案,我不认为“自旋锁”或“忙等待”是正确的方法,CPU 片被浪费,并且整个系统将变得无响应或可能反应迟钝。

有人可以清除这些相互矛盾的想法吗?

【问题讨论】:

【参考方案1】:

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

旋转(不睡觉的另一种等待方式)不太常见,并具有以下效果:

保持 CPU 忙碌,并阻止其他线程使用 CPU(直到/除非旋转线程完成其时间片并被抢占)

可以在您等待的事情发生的那一刻停止旋转(因为您一直在检查该事件,并且您不需要花时间被唤醒,因为你已经醒了)

不调用进入睡眠和再次唤醒所需的 CPU 指令

如果延迟的长度非常短(例如,如果延迟只需要执行 100 CPU 指令)。

【讨论】:

【参考方案2】:

自旋锁会烧毁 CPU 和轮询的资源路径,以便在未发生所需事件的情况下继续浪费资源。

阻塞操作最重要的不同之处在于将 CPU 和相关资源路径排除在外,并在预期发生所需事件的资源上​​安装某种形式的 wait

在多任务或多线程/处理器环境中(很长一段时间以来的常见情况),在所需事件尚未到达时可能进行其他操作,烧毁 CPU 和资源访问路径会导致处理能力的严重浪费和时间。

当我们有一个超线程系统时(就像我认为您在问题中所指的那样),重要的是要注意 CPU 线程被切片的粒度非常高。我会伸出我的脖子来观察所有的事件——你倾向于阻塞——都需要足够的时间才能出现,以补偿他们在解除阻塞之前必须额外等待的小时间片。


我认为J-16s 的观点是针对休眠(阻塞)线程在阻塞状态下未使用其代码和数据空间的情况。这可能会使系统放弃资源(如数据/代码缓存),然后在释放块时需要重新填充这些资源。因此,根据条件,一个块可能会造成更多的资源浪费。 这也是一个有效的注释,应该在设计和实现中进行检查。 但是,在大多数情况下,阻塞通常比自旋锁更好。

【讨论】:

对,我想我会提出一个答案,看看人们是否同意,否则我错了。不过谢谢。是的,j16 带来了不同的视角。这很好。并感谢 nik 强调它。 :)【参考方案3】:

如果在您的应用程序的用例中,上下文切换会比消耗几个 CPU 周期更昂贵,因为您的条件可以保证在短时间内得到满足 时间,那么忙碌的等待可能对你有好处。

否则,您可以通过休眠或cond_wait()ing 来强制放弃 CPU。

我能想到的强制上下文切换的另一种场景如下:

while(condition)
    sleep(0);

【讨论】:

【参考方案4】:

首先,你有一个误解:

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

关于屏蔽通话的问题

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

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

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

【讨论】:

@j-16,这正是我所说的,“阻塞呼叫是可睡眠的”请重新阅读问题。 阻塞调用怎么会占用资源,如果它们正在休眠,它们不会占用任何 CPU。 @Vivek:这里的“资源”主要是内存 @j-16,这里的每个人都在谈论 CPU 周期,因此我认为资源(尽管主观上)显然是 CPU。而且,据我所知,线程切换与进程的上下文切换不同。好吧,这取决于线程如何与内核的 LWP 绑定,但这是另一个特定于实现的主题。哦,好的,您的意思是在您的回答中您将资源称为内存。 @vivek:切换线程不会切换整个内存空间,但会切换寄存器和堆栈——这是上下文切换。即使您忽略内存使用量(这可能很重要),更改堆栈和重新填充缓存也会消耗 CPU 时间。【参考方案5】:

我会尽量做到这一点,因为这里通过其他答案提供了足够的解释,是的,从所有这些答案中学习,我认为应该是一个完整的画面。 ---

我认为应该在系统的响应性与吞吐量之间进行权衡

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

在所有系统响应能力方面,以及 特定或每个进程的响应能力

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

当然,对于特定进程或每个进程的响应性,我们将考虑忙碌等待/自旋锁模型。

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

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

【讨论】:

想补充一点:我希望您能正确权衡自旋锁资源利用效率低下与阻塞模式(通常是细粒度,在硬件中)调度时间片的有效延迟。 是的,尼克,你说得对,整个辩论都围绕着这个权衡。【参考方案6】:

//改编的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;
    

【讨论】:

以上是关于“忙碌等待”与“睡眠”的权衡是啥?的主要内容,如果未能解决你的问题,请参考以下文章

Linux内核中mutex,spinlock的使用

Java 中啥是快速、等待通知或忙等待?

历史推送和替换之间的权衡是啥?

使用对象 ID 与将对象填充到其他类之间的权衡是啥? [关闭]

睡眠模式是啥?

笔记本睡眠是啥意思?有啥用?