Python培训
美国上市教育机构
400-111-8989
今天我们为大家解读什么是线程?如何使用线程等相关内容,本文适合已经入门Python,并且想用线程来提升程序运行速度的Python工程师,一起来看看:
1. 什么是线程
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
在Python3中实现的大部分运行任务里,不同的线程实际上并没有同时运行:它们只是看起来像是同时运行的。
大家很容易认为线程化是在程序上运行两个(或多个)不同的处理器,每个处理器同时执行一个独立的任务。这种理解并不完全正确,线程可能会在不同的处理器上运行,但一次只能运行一个线程。
同时执行多个任务需要使用非标准的Python运行方式:用不同的语言编写一部分代码,或者使用多进程模块multiprocessing,但这么做会带来一些额外的开销。
由于Python默认的运行环境是CPython(C语言开发的Python),所以线程化可能不会提升所有任务的运行速度。这是因为和GIL(Global Interpreter Lock)的交互形成了限制:一次只能运行一个Python线程。
线程化的一般替代方法是:让各项任务花费大量时间等待外部事件。但问题是,如果想缩短等待时间,会需要大量的CPU计算,结果是程序的运行速度可能并不会提升。
当代码是用Python语言编写并在默认执行环境CPython上运行时,会出现这种情况。如果线程代码是用C语言写的,那它们就能够释放GIL并同时运行。如果是在别的Python执行环境(如IPython, PyPy,Jython,IronPython)上运行,请参考相关文档了解它们是如何处理线程的。
如果只用Python语言在默认的Python执行环境下运行,并且遇到CPU受限的问题,那就应该用多进程模块multiprocessing来解决。
在程序中使用线程也可以简化设计。本文中的大部分示例并不保证可以提升程序运行速度,其目的是使设计结构更加清晰、便于逻辑推理。
下面就来看看如何使用线程吧!
2. 创建线程
既然已经对什么是线程有了初步了解,下面让我们来学习如何创建一个线程。
Python标准库提供了threading模块,里面包含将在本文中介绍的大部分基本模块。在这个模块中,Thread类很好地封装了有关线程的子类,为我们提供了干净的接口来使用它们。
要启动一个线程,需要创建一个Thread实例,然后调用.start()方法:
import logging import threading import time def thread_function(name): ("Thread %s: starting", name) time.sleep(2) ("Thread %s: finishing", name) if __name__ == "__main__": format = "%(asctime)s: %(message)s" logging.basicConfig(format=format, level=, datefmt="%H:%M:%S") ("Main : before creating thread") x = threading.Thread(target=thread_function, args=(1,)) ("Main : before running thread") x.start() ("Main : wait for the thread to finish") # x.join() ("Main : all done") 查看日志语句,可以看到__main__部分正在创建并启动线程: x = threading.Thread(target=thread_function, args=(1,)) x.start()
创建线程时,我们需要传递两个参数,第一个参数target是函数名,指定这个线程去哪个函数里面去执行代码,第二个参数args是一个元组类型,指定为这个函数传递的参数。在本例中,Thread运行函数thread_function(),并将1作为参数传递给该函数。
在本文中,我们用连续整数为线程命名。虽然threading.get_ident()方法能够为每一个线程生成唯一的名称,但这些名称通常会比较长,而且可读性差。
这里的thread_function()函数本身没做什么,它只是简单地记录了一些信息,并用time.sleep()隔开。
运行程序(注释掉倒数第二行代码),结果如下:
$ ./single_ Main : before creating thread Main : before running thread Thread 1: starting Main : wait for the thread to finish Main : all done Thread 1: finishing
可以看到,线程Thread在__main__部分代码运行完后才结束。下一节会对这一现象做出解释,并讨论被注释掉那行代码。
2.1. 守护线程
在计算机科学中,守护进程daemon是一类在后台运行的特殊进程,用于执行特定的系统任务。
守护进程daemon在Python线程模块threading中有着特殊的含义。当程序退出时,守护线程将立即关闭。可以这么理解,守护线程是一个在后台运行,且不用费心去关闭它的线程,因为它会随程序自动关闭。
如果程序运行的线程是非守护线程,那么程序将等待所有线程结束后再终止。但如果运行的是守护线程,当程序退出时,守护线程会被自动杀死。
我们仔细研究一下上面程序运行的结果,注意看最后两行。当运行程序时,在__main__部分打印完all done信息后、线程结束前,有一个大约2秒的停顿。
这时,Python在等待非守护线程完成运行。当Python程序结束时,关闭过程的一部分是清理线程。
查看Python线程模块的源代码,可以看到thread ._shutdown()方法遍历所有正在运行的线程,并在每个非守护线程上调用.join()函数,检查它们是否已经结束运行。
因此,程序退出时需要等待,因为守护线程本身会在休眠中等待其他非守护线程运行结束。一旦thread ._shutdown()运行完毕并打印出信息,程序就可以退出。
守护线程这种自动退出的特性很实用,但其实还有其他的方法能实现相同的功能。我们先用守护线程重复运行一下上面的程序,看看结果。只需在创建线程时,添加参数daemon=True。
x = threading.Thread(target=thread_function, args=(1,), daemon=True)现在运行程序,结果如下:
$ ./single_ Main : before creating thread Main : before running thread Thread 1: starting Main : wait for the thread to finish Main : all done Thread 1: finishing
添加参数daemon=True前
$ ./daemon_ Main : before creating thread Main : before running thread Thread 1: starting Main : wait for the thread to finish Main : all done
添加参数daemon=True后
不同的地方是,之前输出的最后一行不见了,说明thread_function()函数没有机会完成运行。这是一个守护线程,所以当__main__部分运行完最后一行代码,程序终止,守护线程被杀死。
2.2. 加入一个线程
守护线程用起来很方便,但如果想让守护线程运行完毕后再结束程序该怎么办?或者想让守护线程运行完后不退出程序呢?
让我们来看一下刚刚注释掉的那行代码:
# x.join()要让一个线程等待另一个线程完成,可以调用.join()函数。如果取消对这行代码的注释,主线程将会暂停,等待线程x完成运行。
这个功能在守护线程和非守护线程上同样适用。如果用.join()函数加入了一个线程,则主线程将一直等待,直到被加入的线程运行完成。
3. 多线程
到目前为止,示例代码中只用到了两个线程:主线程和一个threading.Thread线程对象。
通常,我们希望同时启动多个线程,让它们执行不同的任务。先来看看比较复杂的创建多线程的方法,然后再看简单的。
这个复杂的创建方法其实前面已经展示过了:
import logging import threading import time def thread_function(name): ("Thread %s: starting", name) time.sleep(2) ("Thread %s: finishing", name) if __name__ == "__main__": format = "%(asctime)s: %(message)s" logging.basicConfig(format=format, level=, datefmt="%H:%M:%S") threads = list() for index in range(3): ("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): ("Main : before joining thread %d.", index) thread.join() ("Main : thread %d done", index)
这段代码和前面提到的创建单线程时的结构是一样的,创建线程对象,然后调用.start()方法。程序中会保存一个包含多个线程对象的列表,为稍后使用.join()函数做准备。
多次运行这段代码可能会产生一些有趣的结果:
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
仔细看一下输出结果,三个线程都按照预想的顺序创建0,1,2,但它们的结束顺序却是相反的!多次运行将会生成不同的顺序。查看线程Thread x: finish中的信息,可以知道每个线程都在何时完成。
线程的运行顺序是由操作系统决定的,并且很难预测。很有可能每次运行所得到的顺序都不一样,所以在用线程设计算法时需要注意这一点。
幸运的是,Python中提供了几个基础模块,可以用来协调线程并让它们一起运行。在介绍这部分内容之前,让我们先看看如何更简单地创建一组线程。
4. 线程池
我们可以用一种更简单的方法来创建一组线程:线程池ThreadPoolExecutor,它是Python中concurrent.futures标准库的一部分。(Python 3.2 以上版本适用)。
最简单的方式是把它创建成上下文管理器,并使用with语句管理线程池的创建和销毁。
用ThreadPoolExecutor重写上例中的__main__部分,代码如下:
import concurrent.futures # [rest of code] if __name__ == "__main__": format = "%(asctime)s: %(message)s" logging.basicConfig(format=format, level=, datefmt="%H:%M:%S") with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: (thread_function, range(3))
这段代码创建一个线程池ThreadPoolExecutor作为上下文管理器,并传入需要的工作线程数量。()遍历可迭代对象,本例中是range(3),每个对象生成池中的一个线程。
在with模块的结尾,会让线程池ThreadPoolExecutor对池中的每个线程调用.join()。强烈建议使用线程池ThreadPoolExecutor作为上下文管理器,因为这样就不会忘记写.join()。
注:
使用线程池ThreadPoolExecutor可能会报一些奇怪的错误。例如,调用一个没有参数的函数,()时,线程将抛出异常。
不幸的是,线程池ThreadPoolExecutor会隐藏该异常,程序会在没有任何输出的情况下终止。刚开始调试时,这会让人很头疼。
运行修改后的示例代码,结果如下:
$ ./ Thread 0:starting Thread 1: starting Thread 2: starting Thread 1: finishing Thread 0: finishing Thread 2: finishing
再提醒一下,这里的线程1在线程0之前完成,这是因为线程的调度是由操作系统决定的,并不遵循一个特定的顺序。
感谢您的阅读,以上就是为您解读的什么是线程、如何操作使用线程等相关内容,你都学会了吗?更多Python相关的内容尽在达内Python培训机构官网,敬请关注!
免责声明:内容和图片源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。
填写下面表单即可预约申请免费试听!怕钱不够?可就业挣钱后再付学费! 怕学不会?助教全程陪读,随时解惑!担心就业?一地学习,可全国推荐就业!