Python的time.sleep(0)在Linux中的行为 - 会导致上下文切换吗?
这个问题经常出现,但我找不到明确的答案。
一个不太重要、也不太友好的程序可能会这样做:
while(True):
# do some work
如果你使用其他技术和平台,想让这个程序尽可能多地使用CPU资源,但又想保持礼貌——也就是让其他正在占用CPU资源的程序能够有效地减缓我的速度,你通常会写:
while(True):
#do some work
time.sleep(0)
我看到关于这种方法在Python上运行时是否能达到我希望的效果的信息有些矛盾,尤其是在Linux系统上。这会导致上下文切换吗?这会导致我之前提到的那种行为吗?
补充:为了验证这一点,我们在苹果的OSX系统上做了一个小实验(因为手边没有Linux系统)。这台机器有4个核心,还有超线程功能,所以我们启动了8个程序,代码如下:
while(True):
i += 1
如预期的那样,活动监视器显示这8个进程的CPU使用率都超过了95%(显然,4个核心加上超线程,总共可以达到800%)。然后我们又启动了第九个这样的程序。现在所有9个程序的CPU使用率都在85%左右。接着我们结束了第九个程序,再启动一个程序,代码如下:
while(True):
i += 1
time.sleep(0)
我本希望这个新程序的CPU使用率接近0%,而其他8个程序能保持在95%左右。但结果是,所有九个程序的使用率都在85%左右。所以在苹果的OSX系统上,sleep(0)似乎没有任何效果。
3 个回答
你基本上是在试图抢夺操作系统的CPU调度器的工作。其实,直接调用 os.nice(100)
来告诉调度器你是个优先级很低的任务,这样它就能更好地完成自己的工作,这样可能会更好。
我想你已经从@Ninefingers那里得到了答案,但在这个回答中,我们将尝试深入了解Python的源代码。
首先,Python的time
模块是用C语言实现的。如果你想看看time.sleep
这个函数是怎么实现的,可以查看Modules/timemodule.c。如你所见(不涉及所有平台的具体细节),这个函数会把调用委托给floatsleep函数。
现在,floatsleep
是为了在不同的平台上工作而设计的,但它的行为尽可能保持一致。不过,我们只对类Unix平台感兴趣,所以我们来看看这一部分吧:
...
Py_BEGIN_ALLOW_THREADS
sleep((int)secs);
Py_END_ALLOW_THREADS
如你所见,floatsleep
调用了C语言的sleep函数,而从sleep手册页中可以了解到:
sleep()函数会使调用的线程暂停执行,直到指定的实时秒数(由参数seconds指定)过去,或者……
等等,我们是不是忘了GIL(全局解释器锁)的问题?
这就是Py_BEGIN_ALLOW_THREADS
和Py_END_ALLOW_THREADS
这两个宏发挥作用的地方(如果你对这两个宏的定义感兴趣,可以查看Include/ceval.h)。上面的C代码可以用这两个宏转换为:
Save the thread state in a local variable.
Release the global interpreter lock.
... Do some blocking I/O operation ... (call sleep in our case)
Reacquire the global interpreter lock.
Restore the thread state from the local variable.
关于这两个宏的更多信息可以在C API文档中找到。
希望这些信息对你有帮助。
我从来没有想过这个问题,所以我写了一个脚本:
import time
while True:
print "loop"
time.sleep(0.5)
这只是一个测试。用 strace -o isacontextswitch.strace -s512 python test.py
运行这个脚本会在循环中给你这样的输出:
write(1, "loop\n", 5) = 5
select(0, NULL, NULL, NULL, {0, 500000}) = 0 (Timeout)
write(1, "loop\n", 5) = 5
select(0, NULL, NULL, NULL, {0, 500000}) = 0 (Timeout)
write(1, "loop\n", 5) = 5
select(0, NULL, NULL, NULL, {0, 500000}) = 0 (Timeout)
write(1, "loop\n", 5) = 5
select(0, NULL, NULL, NULL, {0, 500000}) = 0 (Timeout)
write(1, "loop\n", 5)
select()
是一个系统调用,所以是的,你确实在进行上下文切换(严格来说,当你切换到内核空间时并不一定需要上下文切换,但如果有其他进程在运行,你所说的意思是,除非你的文件描述符上有数据可以读取,否则其他进程可以在那之前运行)。有趣的是,延迟发生在对标准输入的选择上。这让 Python 可以在你输入 ctrl+c
这样的事件时打断你的输入,而不必等到代码超时 - 我觉得这很酷。
我应该提到,time.sleep(0)
的情况也是一样,只不过传入的时间参数是 {0,0}
。而且自旋锁并不适合用于除了非常短的延迟之外的任何情况 - multiprocessing
和 threads
提供了等待事件对象的能力。
编辑: 所以我查看了一下 Linux 实际上是怎么做的。在 do_select
的实现中 (fs\select.c
) 做了这样的检查:
if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
wait = NULL;
timed_out = 1;
}
if (end_time && !timed_out)
slack = select_estimate_accuracy(end_time);
换句话说,如果提供了结束时间并且两个参数都是零(!0 = 1,在 C 语言中评估为真),那么等待就被设置为 NULL,选择被认为是超时。然而,这并不意味着这个函数会立刻返回给你;它会遍历你所有的文件描述符并调用 cond_resched
,从而可能允许另一个进程运行。换句话说,发生的事情完全取决于调度器;如果你的进程相比其他进程占用了过多的 CPU 时间,那么很可能会发生上下文切换。如果没有,你正在进行的任务(内核的 do_select 函数)可能会继续执行直到完成。
不过我想重申一下,通常让其他进程更友好的最好方法是使用其他机制,而不是自旋锁。