忙碌等待

✍ dations ◷ 2024-12-23 01:58:02 #反模式,协同控制

在软件工程中,忙碌等待(也称自旋;英语:Busy waiting、busy-looping、spinning)是一种以进程反复检查一个条件是否为真为根本的技术,条件可能为键盘输入或某个锁是否可用。忙碌等待也可以用来产生一个任意的时间延迟,若系统没有提供生成特定时间长度的方法,则需要用到忙碌等待。不同的计算机处理器速度差异很大,特别是一些处理器设计为可能根据外部因素(例如操作系统上的负载)动态调整速率。因此,忙碌等待这种时间延迟技术容易产生不可预知、甚至不一致的结果,除非实现代码来确定处理器执行“什么都不做”循环的速度,或者循环代码明确检查实时时钟。

在某些情况下,忙碌等待是有效的策略,特别是实现自旋锁设计的操作系统上运行对称多处理。不过一般来说,忙碌等待是应该避免的反模式,处理器时间应该用来执行其他任务,而不是浪费在无用的活动上。

对于多核CPU,忙碌等待的优点是不切换线程,避免了由此付出的代价。因此一些多线程同步机制不使用切换到内核态的同步对象,而是以用户态的自旋锁或其派生机制(如轻型读写锁)来做同步,付出的时间复杂度相差3个数量级。忙碌等待可使用一些机制来降低CPU功耗,如Windows系统中调用YieldProcessor,实际上是调用了SIMD指令_mm_pause。

以下的C语言程序示范二个线程共享一个全局变量i,第一个线程用忙碌等待来确认变量i的值是否有改变。

 1 #include <stdio.h> 2 #include <pthread.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5  6 volatile int i = 0; /* i is global, so it is visible to all functions. 7                        It's also marked volatile, because it 8                        may change in a way which is not predictable by the compiler, 9                        here from a different thread. */10 11 /* f1 uses a spinlock to wait for i to change from 0. */12 static void *f1(void *p) {13     while (i == 0) {14         /* do nothing - just keep checking over and over */15     }16     printf("i's value has changed to %d.\n", i);17     return NULL;18 }19 20 static void *f2(void *p) {21     sleep(60);   /* sleep for 60 seconds */22     i = 99;23     printf("t2 has changed the value of i to %d.\n", i);24     return NULL;25 }26 27 int main() {28     int rc;29     pthread_t t1, t2;30     rc = pthread_create(&t1, NULL, f1, NULL);31     if (rc != 0) {32         fprintf(stderr, "pthread f1 failed\n");33         return EXIT_FAILURE;34     }35     rc = pthread_create(&t2, NULL, f2, NULL);36     if (rc != 0) {37         fprintf(stderr, "pthread f2 failed\n");38         return EXIT_FAILURE;39     }40     pthread_join(t1, NULL);41     pthread_join(t2, NULL);42     puts("All pthreads finished.");43     return 0;44 }

上述的程序也可以用C11标准中的条件变量达成。

大多数操作系统和线程库提供了各种各样可以阻止事件过程的系统调用,如锁获取、计时器变化,I/O可用性或信号。使用系统调用来产生延迟会有最简单、最有效、公平且没有竞争危害的结果。一个调用会检查、通知事件等待的调度程序,插入一个适用的记忆障碍,也可以在返回之前执行所请求的I / O操作。当调用者被堵住时,其他进程可以使用CPU。调度器有实现优先级继承所需的信息或其他机制,来避免资源衰竭(英语:Resource starvation)的问题。

在大部分操作系统中,也可以在忙碌等待中加入延迟函数(sleep()),以减少忙碌等待浪费的CPU资源。这可以让线程暂停指定的时间,在此期间线程不会浪费CPU时间。如果循环检查只是检查一些简单的事务,将大部分时间花费在延迟函数,则不太会浪费太多CPU时间。

若程序永远不会结束(如操作系统),可以通过无条件跳转(例如NASM语法中的jmp $)实现无限次的忙碌等待。CPU会永远无条件跳转到程序现在运行到的位置。因此可以用以下的程序取代忙碌等待:

sleep:hltjmp sleep

适当的使用忙碌等待

一些底层编程中可能需要用到需忙碌等待。对每个硬件设备(尤其是偶尔才使用到的硬件)设置中断可能不切实际甚至不可能。有时需要将某种的控制数据写入硬件,在写入后获取设备状态,但状态可能要在写入后数个机器周期后才有效。程序员可以调用操作系统延迟函数,不过这样做可能要耗费更多的时钟周,此时就可以使用忙碌等待。

相关