【译】线程:概念和应用(1)

翻译:老齐算法

译者注:与本文相关图书推荐:《Python大学实用教程》《跟老齐学Python:轻松入门》编程

本文将分两部分刊发。bash

第一部分


Python线程容许程序的不一样部分同时运行,并能够简化设计。若是你对Python有一些经验,而且但愿使用线程为程序加速,那么本文就是为你准备的!微信

什么是线程?

线程是一个独立的流,这意味着你的程序能够同时作两件事,可是,对于大多数Python程序,不一样的线程实际上并不一样时执行,它们只是看起来像是同时执行。多线程

人们很容易认为线程是在程序上运行两个(或更多)不一样的处理器,每一个处理器同时执行一个独立的任务。这种见解大体正确,线程可能在不一样的处理器上运行,但一个处理器一次只能运行一个线程。架构

要同时运行多个任务,不能用Python的标准方式实现,能够用不一样的编程语言,或者多个进程实现,这样作的开发成本就高了。并发

因为用CPython实现了Python业务,线程可能不会加速全部任务,这是GIL(全称Global Interpreter Lock)的缘由,一次只能运行一个Python线程。app

若是某项任务须要花费大量时间等待外部事件,那么就能够应用多线程。若是是须要对CPU占用高而且花费不多时间等待外部事件,多线程可能枉费。编程语言

对于用Python编写并在标准CPython实现上运行的代码,这是正确的。若是你的线程是用C编写的,那么它们就可以释放GIL、并发运行。若是你在不一样的Python实现上运行,也能够查看文档,了解它如何处理线程。ide

若是你正在运行一个标准的Python程序,只使用Python编写,而且有一个CPU受限的问题,那么你应该用多进程解决此问题。

将程序架构为使用线程也能够提升设计的清晰度。你将在下文中学习的大多数示例不必定会运行得更快,由于它们使用线程。在这些示例中使用线程有助于使设计更清晰、更易于推理。

因此,让咱们中止谈论线程并开始使用它!

建立一个线程

如今你已经知道了什么是线程,让咱们来学习如何制做线程。Python标准库提供了线程模块threading,它包含了你将在本文中看到的大部份内容。在这个模块中,Thread是对线程的封装,提供了简单的实现接口。

要建立一个线程,须要建立Thread的实例,而后调用它的.start()方法:

import logging
import threading
import time

def thread_function(name):
    logging.info("Thread %s: starting", name)
    time.sleep(2)
    logging.info("Thread %s: finishing", name)

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")
    logging.info("Main : before creating thread")
    x = threading.Thread(target=thread_function, args=(1,))
    logging.info("Main : before running thread")
    x.start()
    logging.info("Main : wait for the thread to finish")
    # x.join()
    logging.info("Main : all done")
复制代码

若是你查看日志,能够看到在main部分正在建立和启动线程:

x = threading.Thread(target=thread_function, args=(1,))
x.start()
复制代码

用函数thread_function()arg(1,)建立一个Thread实例。在本文中用整数做为线程的名称,threading.get_ident()能够返回线程的名称,但可读性较差。

thread_function()函数的做用不大,它只是记录一些日志消息,在这些消息之间加上time.sleep()

当你执行此程序时,输出将以下所示:

$ ./single_thread.py
Main : before creating thread
Main : before running thread
Thread 1: starting
Main : wait for the thread to finish
Main : all done
Thread 1: finishing
复制代码

你会注意到代码的main部分结束以后,Thread才结束。后面会揭示这么作的缘由。

守护线程

在计算机科学中,daemon是在后台运行的程序。

Python的threading模块对daemon有更具体的含义。当程序退出时,守护线程会当即关闭。考虑这些定义的一种方法是将daemon视为在后台运行的线程,而没必要担忧关闭它。

若是程序中正在执行的Threads不是daemons,则程序将在终止以前等待这些线程完成。然而,若是Threadsdaemons,当程序退出时,它们就终止了。

让咱们更仔细地看看上面程序的输出,最后两行是有点意思的。当运行这个程序时,在__main__打印完all done后以及线程结束以前会暂停大约2秒。

这个暂停是Python等待非后台线程完成。当Python程序结束时,关闭操做是清除线程中的程序。

若是查看threading模块的源代码,你将看到threading._shutdown()方法,它会遍历全部正在运行的线程,并在每个没有设置daemon标志的线程上调用.join()方法。

所以,程序在退出时会等待,由于线程自己正在sleep(time.sleep(2))中。一旦完成并打印了消息,.join() 将返回,程序才能够退出。

一般,这是你想要的,可是咱们还有其余的选择。让咱们首先使用一个daemon线程来重复这个程序。你能够修改Thread实例化时的参数,添加daemon=True:

x = threading.Thread(target=thread_function, args=(1,), daemon=True)
复制代码

如今运行程序时,应看到如下输出:

$ ./daemon_thread.py
Main : before creating thread
Main : before running thread
Thread 1: starting
Main : wait for the thread to finish
Main : all done
复制代码

与前面不一样的是,前面所输出的最后一行在这里没有了。thread_function()没有执行完,它是一个daemon线程,因此当_main__执行到达它的末尾时,程序结束,后台线程也就结束了。

线程实例的.join()方法

守护线程很方便,可是,若是要实现线程彻底执行,而不是被迫退出,应该怎么办?如今让咱们回到原始程序,看看注释掉的那一行:

# x.join()
复制代码

要让一个线程等待另外一个线程完成,能够调用.join()。取消对该行的注释,主线程将暂停并等待线程x,直到它运行结束。

你是否在程序中用守护线程或普通线程测试了这个问题?这并不重要。若是执行某个线程的.join()方法,该语句将一直等待,直到每一个线程都完成。

使用多线程

到目前为止,示例代码只使用了两个线程:一个是主线程,另外一个是以threading.Thread对象开始的线程。

一般,您会但愿启动更多线程并让它们作一些有趣的工做。咱们先来看看比复杂的方法,而后再看比较简单的方法。

启动多线程比较复杂的方法是你已经知道的:

import logging
import threading
import time

def thread_function(name):
    logging.info("Thread %s: starting", name)
    time.sleep(2)
    logging.info("Thread %s: finishing", name)

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")

    threads = list()
    for index in range(3):
        logging.info("Main : create and start thread %d.", index)
        x = threading.Thread(target=thread_function, args=(index,))
        threads.append(x)
        x.start()

    for index, thread in enumerate(threads):
        logging.info("Main : before joining thread %d.", index)
        thread.join()
        logging.info("Main : thread %d done", index)
复制代码

这段代码使用与上面看到的相同机制来启动线程,建立一个Thread实例对象,而后调用.start()。程序中生成一个由Thread实例组成的列表,后面再调用每一个实例.join()方法。

屡次运行此代码可能会产生一些有趣的结果。下面是个人机器的输出示例:

$ ./multiple_threads.py
Main : create and start thread 0.
Thread 0: starting
Main : create and start thread 1.
Thread 1: starting
Main : create and start thread 2.
Thread 2: starting
Main : before joining thread 0.
Thread 2: finishing
Thread 1: finishing
Thread 0: finishing
Main : thread 0 done
Main : before joining thread 1.
Main : thread 1 done
Main : before joining thread 2.
Main : thread 2 done
复制代码

若是仔细检查输出,你将看到全部三个线程都按照你可能指望的顺序开始,但在本例中,它们是按照相反的顺序完成的!屡次运行将产生不一样的排序,能够经过查找Thread x: finishing消息来了解每一个线程什么时候完成。

线程的运行顺序由操做系统决定,很难预测,它可能(并且极可能)因运行而异,所以在设计使用线程的算法时须要注意这一点。

幸运的是,Python提供了几个模块,你稍后将看到这些模块用来帮助协调线程并使它们一块儿运行。在此以前,让咱们看看如何更简单地管理一组线程。

使用ThreadPoolExecutor

有一种比上面看到的更容易启动多线程的方法,它被称为ThreadPoolExecutor,是标准库中的concurrent.futures的一员(从Python3.2开始)。

建立它的最简单方法是使用上下文管理器的with语句,用它实现对线程池的建立和销毁。

下面是为了使用ThreadPoolExecutor而重写的上一个示例中的__main__部分代码:

import concurrent.futures

# [rest of code]

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")

    with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
        executor.map(thread_function, range(3))
复制代码

代码建立了一个ThreadPoolExecutor做为上下文管理器,告诉它须要在线程池中有多少个工做线程。而后它使用.map()遍历可迭代对象,在上面的例子中是range(3),将每一个可迭代对象传递给线程池中的一个线程。

with语句块的尾部,默认会调用ThreadPoolExecutor的每一个线程的.join()方法,建议你尽量使用ThreadPoolExecutor做为上下文管理器,这样你就永远不会忘记对执行线程.join()

注意:使用ThreadPoolExecutor可能会致使一些混乱的错误。

例如,若是调用不带参数的函数,但在.map()中传了参数,则线程应当抛出异常。

不幸的是,ThreadPoolExecutor隐藏了该异常,而且(在上面的状况下)程序将在没有输出的状况下终止。一开始调试可能会很混乱。

运行正确的示例代码将生成以下输出:

$ ./executor.py
Thread 0: starting
Thread 1: starting
Thread 2: starting
Thread 1: finishing
Thread 0: finishing
Thread 2: finishing
复制代码

一样,请注意Thread 1是在Thread 0以前完成的,线程执行顺序的调度是由操做系统完成的,所遵循的计划也不易理解。

未完待续

关注微信公众号:老齐教室。读深度文章,得精湛技艺,享绚丽人生。

相关文章
相关标签/搜索