python 的线程

2019-05-07 13:59:12   最后更新: 2019-05-07 15:10:56   访问数量:200




上一篇文章中,我们详细介绍了 python 中的协程

python 的协程

 

python 通过 yeild 关键字让出 CPU 的执行,实现类似于系统中断的并发工作,这就是被称为“微线程”的 python 协程调度机制

但是,这并不是真正意义上的并发,几乎在所有编程语言中,都提供了多线程并发的机制,python 也同样提供了多线程并发机制,本文我们就来详细介绍 python 中的线程机制

 

 

操作系统中的线程

此前,我们曾经介绍过 linux 环境中的线程和相关的 api

线程的基本概念

 

一个程序的一次执行就是一个进程,而进程中可能包含多个线程,线程是 CPU 调度的最小单位,同一个进程中的多个线程共享了进程中的程序文本、全局内存和堆内存、栈以及文件描述符等资源,而同一个计算机上的多个进程则共享文件系统、磁盘、打印机等硬件资源

线程的切换相对于进程切换耗费的资源更少,因此效率更高,尤其是在同一个进程中的若干个线程之间切换,这是因为进程的切换需要执行系统陷阱、上下文切换和内存与高速缓存的刷新,而由于同一进程中的多个线程共享了这些资源,在线程切换过程中,系统无需对这些资源进行任何操作,因此可以获得更高的效率

可见,线程调度是程序设计中一个非常重要且实用的技术

 

thread 与 threading

python 标准库中维护线程的模块有两个 -- thread 和 threading

由于 thread

模块在很多方面存在不尽如人意的问题,例如在多线程并发环境中,当主线程退出时,所有子线程会随之立即退出,甚至不会进行任何清理工作,这通常是无法接受的,所以一般并不建议使用

在 python3 中 thread 模块已经被更名为 _thread 模块,以便从名字上说明其不被推荐使用

如果你熟悉 java 的线程模型,你会发现 python 的线程模型与 java 的非常类似,没错,python 的线程模型就是参照 java 线程模型设计的,但 python 的线程目前还没有优先级,没有线程组,线程还不能被销毁、停止、暂停、恢复或中断

 

threading 模块中的类

threading 模块包含下列对象:

threading 模块中的对象
对象描述
Thread执行线程对象
Timer运行前等待一定时间的执行线程对象
Lock锁对象
Condition条件变量对象,用于描述线程同步中的条件变量
Event事件对象,用于描述线程同步中的事件
Semaphore信号量对象,用于描述线程同步中的计数器
BoundedSemaphore存在阈值信号量对象
Barrier栅栏对象,线程同步中让多个线程执行到指定位置

 

threading 模块中最重要的类就是 Thread 类

每个 Thread 对象就是一个线程

下面是 Thread 类中包含的属性和方法

Thread 类属性及成员方法
属性备注
name线程名称
ident线程标识符
deamonbool 类型,表示该线程是否为守护线程
start()开始执行线程
run()用于定义线程功能,通常在子类中由开发者复写
join(timeout=None)直到启动的线程终止或到超时时间前一直挂起
is_alive()返回 bool 类型,表示该线程是否存活

 

创建线程

有两种方法可以创建线程,但更推荐第二种:

 

以一个函数或一个可调用类实例为参数创建 Thread 对象

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 秒

 

派生 Thread 并创建子类实例

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())

 

 

运行与上面通过函数实现的例子是完全一致的

由于类中可以添加私有成员来保存成员方法运行结果或其他数据,这个方法显得更为灵活

 

start 方法

只有在 Thread 对象的 start 方法被调用后才会开始线程活动

start 方法在一个线程里最多只能被调用一次,否则会抛出 RuntimeError

start 最终执行的逻辑代码就是 Thread 类的 run 方法

 

join 方法

join 方法有一个可选的 timeout 参数,这个方法会阻塞调用这个方法的线程,直到被调用 join() 的线程终结或达到 timeout 秒数

当然,一个线程可以被 join 很多次,但 join 当前线程会导致死锁

如果被 join 的线程不处于 alive 状态,则会引起 RuntimeError 异常

 

线程的终止

在线程中,可以通过 sys.exit() 方法或抛出 SystemExit 异常来使线程退出

但是,在线程外,你不能直接终止一个线程

 

除了最重要的 Thread 类,threading 模块中还提供了下面的几个有用的函数

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

 






技术帖      python      线程      thread      并发      同步     


京ICP备15018585号