上一篇文章中,我们详细介绍了 python 中的协程。 一文讲透 python 协程
python 通过 yeild 关键字让出 CPU 的执行,实现类似于系统中断的并发工作,这就是被称为“微线程”的 python 协程调度机制。 但是,这并不是真正意义上的并发,几乎在所有编程语言中,都提供了多线程并发的机制,python 也同样提供了多线程并发机制,本文我们就来详细介绍 python 中的线程机制。
此前,我们曾经介绍过 linux 环境中的线程和相关的 api。
一个程序的一次执行就是一个进程,而进程中可能包含多个线程,线程是 CPU 调度的最小单位,同一个进程中的多个线程共享了进程中的程序文本、全局内存和堆内存、栈以及文件描述符等资源,而同一个计算机上的多个进程则共享文件系统、磁盘、打印机等硬件资源。 线程的切换相对于进程切换耗费的资源更少,因此效率更高,尤其是在同一个进程中的若干个线程之间切换,这是因为进程的切换需要执行系统陷阱、上下文切换和内存与高速缓存的刷新,而由于同一进程中的多个线程共享了这些资源,在线程切换过程中,系统无需对这些资源进行任何操作,因此可以获得更高的效率。 可见,线程调度是程序设计中一个非常重要且实用的技术。
python 标准库中维护线程的模块有两个 — thread 和 threading。 由于 thread。 模块在很多方面存在不尽如人意的问题,例如在多线程并发环境中,当主线程退出时,所有子线程会随之立即退出,甚至不会进行任何清理工作,这通常是无法接受的,所以一般并不建议使用。 在 python3 中 thread 模块已经被更名为 _thread 模块,以便从名字上说明其不被推荐使用。 如果你熟悉 java 的线程模型,你会发现 python 的线程模型与 java 的非常类似,没错,python 的线程模型就是参照 java 线程模型设计的,但 python 的线程目前还没有优先级,没有线程组,线程还不能被销毁、停止、暂停、恢复或中断。
threading 模块包含下列对象:
对象 | 描述 |
---|---|
Thread | 执行线程对象 |
Timer | 运行前等待一定时间的执行线程对象 |
Lock | 锁对象 |
Condition | 条件变量对象,用于描述线程同步中的条件变量 |
Event | 事件对象,用于描述线程同步中的事件 |
Semaphore | 信号量对象,用于描述线程同步中的计数器 |
BoundedSemaphore | 存在阈值信号量对象 |
Barrier | 栅栏对象,线程同步中让多个线程执行到指定位置 |
threading 模块中最重要的类就是 Thread 类。 每个 Thread 对象就是一个线程。 下面是 Thread 类中包含的属性和方法。
属性 | 备注 |
---|---|
name | 线程名称 |
ident | 线程标识符 |
deamon | bool 类型,表示该线程是否为守护线程 |
start() | 开始执行线程 |
run() | 用于定义线程功能,通常在子类中由开发者复写 |
join(timeout=None) | 直到启动的线程终止或到超时时间前一直挂起 |
is_alive() | 返回 bool 类型,表示该线程是否存活 |
有两种方法可以创建线程,但更推荐第二种:
from threading import Thread
from time import sleep, ctime
def sleep_func(i):
print('start_sleep[%s]' % i)
sleep(i+1)
print('end_sleep[%s]' % i)
if __name__ == '__main__':
print('start at %s'% ctime())
threads = list()
for i in range(3):
t = Thread(target=sleep_func, args=[i])
threads.append(t)
for i in range(3):
threads[i].start()
for i in range(3):
threads[i].join()
print('end at %s' % ctime())
打印出了:
start at Mon May 6 12:25:18 2019 start_sleep[0] start_sleep[1] start_sleep[2] end_sleep[0] end_sleep[1] end_sleep[2] end at Mon May 6 12:25:21 2019 并且我们观察到每过 1 秒便打印出一个 end_sleep,这说明他们确实是并行执行的。 本应共执行 1+2+3 = 6 秒的,由于线程的并发执行,实际只用了 3 秒。
from threading import Thread
from time import sleep, ctime
class myThread(Thread):
def __init__(self, nsec):
super().__init__()
self.nsec = nsec
def run(self):
print('start_sleep[%s]' % self.nsec)
sleep(self.nsec + 1)
print('end_sleep[%s]' % self.nsec)
if __name__ == '__main__':
print('start at %s'% ctime())
threads = list()
for i in range(5):
t = myThread(i)
threads.append(t)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print('end at %s' % ctime())
运行与上面通过函数实现的例子是完全一致的。 由于类中可以添加私有成员来保存成员方法运行结果或其他数据,这个方法显得更为灵活。
只有在 Thread 对象的 start 方法被调用后才会开始线程活动。 start 方法在一个线程里最多只能被调用一次,否则会抛出 RuntimeError。 start 最终执行的逻辑代码就是 Thread 类的 run 方法。
join 方法有一个可选的 timeout 参数,这个方法会阻塞调用这个方法的线程,直到被调用 join() 的线程终结或达到 timeout 秒数。 当然,一个线程可以被 join 很多次,但 join 当前线程会导致死锁。 如果被 join 的线程不处于 alive 状态,则会引起 RuntimeError 异常。
在线程中,可以通过 sys.exit() 方法或抛出 SystemExit 异常来使线程退出。 但是,在线程外,你不能直接终止一个线程。
除了最重要的 Thread 类,threading 模块中还提供了下面的几个有用的函数。
函数 | 说明 |
---|---|
active_count() | 返回当前活动的 Thread 对象个数 |
current_thread() | 返回当前线程的 Thread 对象 |
enumerate() | 返回当前活动的 Thread 对象列表 |
settrace(func) | 为所有线程设置一个 trace 函数 |
setprofile(func) | 为所有线程设置一个 profile 函数 |
local() | 创建或获取线程本地数据对象 |
stack_size(size=0) | 返回新创建线程的栈大小或为后续创建的线程设定栈的大小 为 size |
get_ident() | 返回当前线程的 “线程标识符”,它是一个非零的整数 |
main_thread() | 返回主 Thread 对象。一般情况下,主线程是Python解释器开始时创建的线程 |
上面我们通过一个实际的例子已经看到,三个线程分别 sleep 1、2、3 秒,执行结果却只话费了 3 秒,足以见得并发环境下的性能优势。 然而,众所周知,python 解释器有多个版本的实现,其中 CPython 以其优秀的执行效率而被广泛使用,也成为了 python 的默认解释器,另一个被广泛使用的是 PyPy 解释器,这两个解释器都有一个先天缺陷,那就是并非线程安全,这是出于在高性能 和复杂度之间做出的让步。 由于 python 解释器 CPython、PyPy 的实现限制,导致实际执行中会设置全局解释安全锁(GIL),一次只允许使用一个线程执行 Python 字节码,因此,一个 Python 进程通常不能同时使用多个 CPU 核心,多线程的程序也并不总是真的在并发执行的,但这并不是 python 语言本身的限制,Jython 与 IronPython 并没有这样的限制。 即便如此,所有标准库中的阻塞式 IO 操作,在等待操作系统返回结果时都会释放 GIL,因此对于 IO 密集型程序,使用多线程并发是可以有效提升性能的,例如我们可以让多个线程可以同时等待或接收 IO操作的返回数据或者在一个线程执行下载任务的同时,另一个线程负责显示下载进度。 time.sleep 操作也是一样,time.sleep 操作会立即释放 GIL 锁,并让线程阻塞等待参数传入的秒数,直到此后才再次请求获取 GIL 锁,这就是上文例子中多线程并发缩短了执行时间的原因。 但对于 CPU 密集型程序,python 线程则显得有些无力,不过这并不是没有办法去优化,我们后文会详细介绍。
斐波那契数列的计算是一个典型的 CPU 密集型操作,下面的例子展示了分别在串行环境与并发环境下计算10次斐波那契数列第 100000 个元素的耗时:
import time
from threading import Thread
class fibThread(Thread):
def __init__(self, stop):
super().__init__()
self.stop = stop
self.result = None
def run(self):
self.result = fib(self.stop)
def fib(stop):
a = 0
b = 1
for _ in range(stop):
a, b = a + b, a
return a
if __name__ == '__main__':
stime = time.time()
for _ in range(10):
fib(100000)
print('serial time %ss' % (time.time() - stime))
stime = time.time()
threads = list()
for _ in range(10):
t = fibThread(100000)
threads.append(t)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print('concurrent time %ss' % (time.time() - stime))
打印出了:
serial time 0.9864494800567627s concurrent time 0.9564151763916016s
虽然从结果上,多线程并发确实有着略微的性能提升,但远远没有达到我们预期的优化 90% 这个问题如何进一步优化,敬请关注接下来的文章。
《python 核心编程》。 https://docs.python.org/zh-cn/3.6/library/threading.html。