龙空技术网

Python | 优雅地实现等待

VT聊球 873

前言:

今天我们对“python等待10秒代码”大致比较看重,我们都需要分析一些“python等待10秒代码”的相关文章。那么小编在网上收集了一些对于“python等待10秒代码””的相关文章,希望同学们能喜欢,小伙伴们快快来学习一下吧!

对于许多类型的应用程序,有时需要暂停程序的运行,直到出现某些外部条件。可能需要等到另一个线程完成,或者可能需要等到正在监视的磁盘目录中出现新文件。

在这种情况下,需要找出一种方法叫脚本等待。正确的操作并不像听起来那么容易!

在本文中,我将向你展示几种不同的等待方式。所有的示例都将使用Python,但我将要介绍的概念适用于所有编程语言。

一个典型示例

为了向你展示这些等待模式,请看下面的示例:

from random import randomimport threadingimport timeresult = Nonedef background_calculation():    # here goes some long calculation    time.sleep(random() * 5 * 60)    # when the calculation is done, the result is stored in a global variable    global result    result = 42def main():    thread = threading.Thread(target=background_calculation)    thread.start()    # TODO: wait here for the result to be available before continuing!    print('The result is', result)if __name__ == '__main__':    main()

程序中background_calculation()函数执行一些速度较慢的计算。为了使这个例子更简单,我在函数中使用了time.sleep()调用一个随机值,随机值最长为5分钟。最后,全局变量result的值为42。

主程序函数在单独的线程中启动后台计算,然后等待线程完成其工作,最后打印全局变量的值。这个版本函数没有实现等待,你可以在需要等待的地方看到一个TODO注释。

接下来,我将向你展示一些不同的方法来实现此处的等待,从最坏的开始,以我的方式达到最好的结果。

糟糕的情形:忙等待

执行此等待的最简单和最直观的方法是使用while循环:

 # wait here for the result to be available before continuing    while result is None:        pass

如果要尝试此操作,可以对以下完整脚本进行复制/粘贴:

from random import randomimport threadingimport timeresult = Nonedef background_calculation():    # here goes some long calculation    time.sleep(random() * 5 * 60)    # when the calculation is done, the result is stored in a global variable    global result    result = 42def main():    thread = threading.Thread(target=background_calculation)    thread.start()    # wait here for the result to be available before continuing    while result is None:        pass    print('The result is', result)if __name__ == '__main__':    main()

这是一种很糟糕的等待方式。你能告诉我为什么吗?

如果你想体验它,可以在你的系统上尝试这个脚本。脚本运行后,在Windows上打开任务管理器,在Mac上打开活动监视器。如果你偏爱命令行,也可以执行top,看看CPU的使用情况,并注意它是如何暴增的。

这个while循环看起来是一个空循环,实际上大部分情况下都是如此。唯一的例外是需要反复检查循环退出条件,以确定何时退出循环。因此,当循环体完全为空时,Python被迫不断地计算result is None。

实际上,循环为空的事实使Python完全专注于尽可能快地重复这个计算,消耗大量的CPU,并使在该CPU上运行的其他所有计算都慢了很多!

这种类型的等待通常称为忙等待。在这种情况下,一个CPU做了很多无用功,我们称之为空转。千万别这样。

坏方案:在忙等待中使用睡眠函数

有趣的是,在上一节中的忙等待示例中,有人会认为拥有空循环应该会减少CPU的工作,但事实上恰恰相反。

因此,对前一个解决方案的明显改进是在while循环中添加一些东西,这会给CPU提供一个制动器,从疯狂地检查while循环条件中退出。

我相信你们中的很多人可能猜到用什么方法实现了,那就是调用sleep函数:

 # wait here for the result to be available before continuing    while result is None:        time.sleep(15)

如果你想在本地运行它,下面是完整的脚本:

from random import randomimport threadingimport timeresult = Nonedef background_calculation():    # here goes some long calculation    time.sleep(random() * 5 * 60)    # when the calculation is done, the result is stored in a global variable    global result    result = 42def main():    thread = threading.Thread(target=background_calculation)    thread.start()    # wait here for the result to be available before continuing    while result is None:        time.sleep(15)    print('The result is', result)if __name__ == '__main__':    main()

函数time.sleep()的参数传入秒数,该时间内程序暂停执行。上面的例子中,在每个循环中休眠15秒,这意味着Python只会以每分钟四次的速率来评估循环的退出条件,而与先前版本中的速度一样快。

在这15秒的睡眠中,CPU将不执行本程序的任何操作,则可以承担其他进程中的工作。

如果你尝试这个版本的程序,会发现脚本在等待时没有使CPU工作过度,因此你可能认为我们现在有了完美的解决方案。但是,我把这个版本的程序也称为坏方案,不是吗?

虽然这个解决方案比前一个好很多,但有两个问题仍然使它不太理想。首先,这个循环仍然可以称为忙等待。

它使用的CPU比前一个要少得多,但我们仍然有一个正在旋转的CPU。我们只是通过降低运算的频率使它变得可以忍受。

在我看来,第二个问题更令人关注。假设正在进行此计算的后台任务只需61秒即可完成其工作并生成结果。

如果我们的等待循环与任务同时开始,它将在0、15、30、45、60和75秒检查结果变量的值。第60秒的检查仍然返回False,因为后台任务还有一秒才能完成,所以第75秒的检查将导致循环退出。

你看到问题了吗?循环在75秒退出,但是后台任务在61秒完成,所以等待延长了额外的14秒!

虽然这种类型的等待非常常见,但它有一个冲突问题,即等待的时长是在循环中所设置的睡眠时长的倍数。

如果睡眠时间较少,等待时间会更精确,但是CPU使用率会因为忙等待而上升。如果睡眠时间较多,CPU使用率就更少,但你可能最终等待的时间比需要的时间要长得多。

优秀方案1:连接线程

假设我们希望等待尽可能有效,等待在某个线程产生结果的确切时刻结束,怎么才能做到呢?

仅使用Python逻辑来实现的解决方案(如前两个)行不通,因为要确定线程是否完成,我们需要运行一些Python代码。

如果我们经常运行检查,就会占用大量的CPU;如果我们不经常运行检查,就会错过线程完成的确切时刻。我们已经在前两部分中清楚地看到了这一点。

为了能够有效地等待,我们需要操作系统的外部帮助。操作系统可以在发生某些事件时有效地通知我们用程序,特别是,它可以告诉我们线程何时退出。这一操作被称为连接线程。

Python标准库中的threading.Thread有一个join()方法,该方法将在线程退出的确切时刻得到返回值:

# wait here for the result to be available before continuing    thread.join()

以下是完整的脚本:

from random import randomimport threadingimport timeresult = Nonedef background_calculation():    # here goes some long calculation    time.sleep(random() * 5 * 60)    # when the calculation is done, the result is stored in a global variable    global result    result = 42def main():    thread = threading.Thread(target=background_calculation)    thread.start()    # wait here for the result to be available before continuing    thread.join()    print('The result is', result)if __name__ == '__main__':    main()

调用join()与time.sleep()一样都会产生阻塞,但它不是在固定时间阻塞,而是在后台线程运行时阻塞。在线程完成时,join()函数执行,应用程序可以继续。操作系统使高效的等待变得容易多了!

优秀方案2:等待事件

如果你需要等待线程完成,那么我在上一节中介绍的模式就是你应该使用的。但是,当然,在许多其他情况下,你可能需要等待线程以外的事情,那么,如何等待某种不绑定到线程的普通事件或其他操作系统资源呢?

为了向你展示如何做到这一点,我将修改我一直使用的示例中的背景线程,使其更加复杂。

这个线程仍然会产生一个结果,但是它不会马上退出,它将继续运行并执行更多工作:

from random import randomimport threadingimport timeresult = Nonedef background_calculation():    # here goes some long calculation    time.sleep(random() * 5 * 60)    # when the calculation is done, the result is stored in a global variable    global result    result = 42    # do some more work before exiting the thread    time.sleep(10)def main():    thread = threading.Thread(target=background_calculation)    thread.start()    # wait here for the result to be available before continuing    thread.join()    print('The result is', result)if __name__ == '__main__':    main()

如果运行上述版本的示例,结果报告将延迟10秒。因为在生成结果后,线程将保持运行那么长时间。但是,我们想在有结果的确切时刻报告结果。

像这样,你需要在任意条件下实现等待,可以使用Event对象,它来自Python标准库中的threading包。以下是创建事件的过程:

result_available = threading.Event()

Events have a wait() method, which we will use to write our wait:

Event实例有一个wait()方法,我们将使用它来编写等待:

# wait here for the result to be available before continuing    result_available.wait()

Event.wait()和Thread.join()的区别在于后者预先指定为等待特定事件,即线程的结束。前者是一个通用事件,可以等待任何事件,那么,如果这个事件对象可以在任何情况下等待,我们如何告诉它何时结束等待?

为此,Event对象有一个set()方法,在后台线程设置result全局变量后,它可以立即设置事件,从而导致等待它的任何代码解除阻塞:

 # when the calculation is done, the result is stored in a global variable    global result    result = 42    result_available.set()

下面是这个例子的完整代码:

from random import randomimport threadingimport timeresult = Noneresult_available = threading.Event()def background_calculation():    # here goes some long calculation    time.sleep(random() * 5 * 60)    # when the calculation is done, the result is stored in a global variable    global result    result = 42    result_available.set()    # do some more work before exiting the thread    time.sleep(10)def main():    thread = threading.Thread(target=background_calculation)    thread.start()    # wait here for the result to be available before continuing    result_available.wait()    print('The result is', result)if __name__ == '__main__':    main()

所以,在这里你可以看到后台线程和主线程是如何围绕这个Event对象同步的。

优秀方案3:显示进度百分比时等待

Event对象的一个优点是它们是通用的,所以如果你运用一点创造力,会发现很多情况下它们都是有用的。例如,在编写后台线程函数时,请考虑以下常见模式:

exit_thread = Falsedef background_thread():    while not exit_thread:        # do some work        time.sleep(10)

在这里,我们试图编写一个线程,这个线程可以通过将全局变量exit_thread设置为True来优雅地终止。这是一个非常常见的模式,但现在你可能确定了为什么这不是一个很好的解决方案,对吧?

从exit_thread变量设置到线程实际退出时,最多需要10秒。也就是说,不必计算线程到达sleep语句之前可能经过的额外时间。

借助Event.wait()方法的timeout参数,我们可以使用Event对象以更有效的方式编写线程:

exit_thread = threading.Event()def background_thread():    while True:        # do some work        if exit_thread.wait(timeout=10):            break

通过这个实现,我们已经用Event对象的智能等待替换了固定时间的睡眠。在每次迭代结束时,仍睡眠10秒,但是如果事件的set()方法在其他地方被调用的同时,线程被困在exit_thread.wait(timeout=10)中,那么调用将立即返回True,线程将退出。

如果超时时间达到10秒,则wait()调用返回False,线程继续运行循环,因此它与调用time.sleep(10)的结果相同。

如果在线程没有运行循环时,程序的其他部分调用exit_thread.set(),那么线程将继续运行。

但当它到达exit_thread.wait()调用时,它将立即返回True并退出。能够在不必等待太久的情况下终止线程的秘密是:确保经常检查事件对象。

让我用这个timeout参数向你展示一个更完整的示例。我要做的是从上一节中获取代码,并在等待过程中展开它以显示完成的百分比。

首先,让我们将进度报告添加到后台线程。在原来的版本中,睡眠的随机数秒最多达到300,也就是5分钟。为了报告这段时间内的任务进度,我将用一个循环替换单个sleep。

该循环运行100个迭代,在每个迭代中稍微休眠,这将使我有机会报告每个迭代中的进度百分比。因为大睡眠持续了300秒,现在我要分成100次睡眠,每次最多3秒。

总的来说,这项任务将花费相同的随机时间,但是将工作划分为100份可以很容易地报告完成的百分比。

下面是对后台线程的更改,用于报告进度全局变量中的进度百分比:

progress = 0def background_calculation():    # here goes some long calculation    global progress    for i in range(100):        time.sleep(random() * 3)        progress = i + 1    # ...

现在我们可以构建一个更智能的等待,每5秒报告完成的百分比:

 # wait here for the result to be available before continuing    while not result_available.wait(timeout=5):        print('\r{}% done...'.format(progress), end='', flush=True)    print('\r{}% done...'.format(progress))

这个新while循环将等待result_available事件长达5秒,并以此作为退出条件。如果在这个时间间隔内没有发生任何事情,那么wait()将返回False。

我们进入循环,在循环中打印progress变量的当前值。请注意,我使用\r字符和print()函数的end=、flush=True参数来防止终端跳转到下一行。

这个技巧允许我们将内容打印在一行,因此每个进度行将在前一行的上方打印。

当背景计算的Event对象调用set()时,循环将退出,因为wait()将立即返回True,此时,我再打印一次,这次使用默认的行结束符。这样我就打印了最后的百分比,并且终端已经准备好打印下一行结果了。

如果你想运行它或更详细地研究它,下面是完整的代码:

from random import randomimport threadingimport timeprogress = 0result = Noneresult_available = threading.Event()def background_calculation():    # here goes some long calculation    global progress    for i in range(100):        time.sleep(random() * 3)        progress = i + 1    # when the calculation is done, the result is stored in a global variable    global result    result = 42    result_available.set()    # do some more work before exiting the thread    time.sleep(10)def main():    thread = threading.Thread(target=background_calculation)    thread.start()    # wait here for the result to be available before continuing    while not result_available.wait(timeout=5):        print('\r{}% done...'.format(progress), end='', flush=True)    print('\r{}% done...'.format(progress))    print('The result is', result)if __name__ == '__main__':    main()
更多方法!

Event对象并不是在应用程序中解决等待问题的唯一方法,还有更多的方法。其中一些方法可能比它更合适,这取决于你在等待什么。

如果需要查看文件目录并在文件被删除时或在现有文件被修改时对文件进行操作,则Event不会有用,因为设置事件的条件在应用程序外部。

在这种情况下,你需要使用操作系统提供的工具来监视文件系统事件。在Python中,可以使用watchdog包,它封装了针对不同操作系统中的文件监视API。

如果需要等待子进程结束,则子进程包提供了一些用于启动和等待进程的函数。

如果你需要从网络socket读取数据,socket的默认配置将使你的读取阻塞,直到数据到达,因此这是一个有效的等待。如果需要在多个socket或其他文件上实现等待,那么Python标准库中的select模块封装了操作系统中实现有效等待的函数。

如果要编写生成且/或使用者数据的程序,则可以使用Queue对象。“生产者”将项目添加到队列中,而“消费者”则高效地等待从队列中获取项目。

正如你所看到的,在大多数情况下,操作系统提供了有效的等待机制,所以你只需要弄清楚如何在Python中访问这些机制。

异步等待

如果你使用的是asyncio包,则可以访问与这些类型相似的等待函数。例如,有一些asyncio.Event和asyncio.Queue对象是根据标准库中的原始对象建模的,但它们是基于异步类型的。

结论

建议你使用我提供的所有示例来熟悉这些技术,并最终使用它们来替换你的代码中效率低下的time.sleep()!

标签: #python等待10秒代码 #python获取当前分钟 #python 进程等待