thread 和 threading 模块都能够实现 python 中的多线程,一般而言使用 threading 更加方便,因为 thread 有很多的缺点,例如当主线程结束后,所以子线程都会强制终止掉,没有警告也没有正常的清理工作。所以一般情况下更推荐使用 threading 模块。不过出于学习的目的,我们两个模块都来看一下。
声明:以下说明都是针对于 python2.7,其他版本可能会存在差异。
在进行代码学习之前,我们要先来了解 python 的 GIL,也就是全局解释器锁。这个锁保证了同一时刻只能有一个线程运行。
等等……我明明要使用多线程,为什么这个锁却保证只有一个线程运行,这样岂不是无法并发了吗?这岂不是坑爹吗?
其实,我们知道 python 是一般解释性的语言,也就是我们的代码要经过解释器解释后才能运行,而一个python的进程只有一个python解释器。我们知道 cpu 对线程的调度是无序的,随机的,所以我们无法保证代码的执行顺序。例如我在前面声明了一个变量,后面使用这个变量,但是在 cpu 进行调度的时候,我们后面的代码先运行了,而此时我们的变量却没有声明,这样肯定会导致各种 BUG。GIL 的存在就是为了保证代码的执行不会混乱。在多线程环境中,Python 虚拟机按以下方式执行:
1.设置 GIL
2.切换到一个线程去运行
3.运行:
a. 指定数量的字节码指令,或者
b. 线程主动让出控制(可以调用 time.sleep(0))
4.把线程设置为睡眠状态
5.解锁 GIL
6.再次重复以上所有步骤
也就是说我将代码分块了,将有关系的代码放在一块,这样有依赖性的代码就能一起执行了,而没有关系的代码就可以分开执行了。不过总体而言,还是只有一个解释器在运行,能同时运行的代码也只有一块,只不过做了分离。
所以也有很多的人说 python 的多线程就是鸡肋,作用不是太大。但是也不能完全这样说。
例如,对所有面向 I/O 的(会调用内建的操作系统 C 代码的)程序来说,GIL 会在这个 I/O 调用之前被释放,以允许其它的线程在这个线程等待 I/O 的时候运行。如果某线程并未使用很多 I/O 操作,它会在自己的时间片内一直占用处理器(和 GIL)。也就是说,I/O 密集型的 Python 程序比计算密集型的程序更能充分利用多线程环境的好处。
在进行文件读写操作,或者爬虫在等待网页下载的时候,就比较适合使用多线程了。
在讲完 python 是如何实现多线程和上面时候使用多线程之后,下面就来正式开始模块的学习。
thread 模块
安装我之前的讨论,在学习任何python内容是,都先调用 help() 函数,查看其内置的帮助文档。帮助文档中有很多的内容,我们先来看看 FUNCTIONS 部分。
1. allocate() / allocate_lock()
我们可以看到其说明是一样的。 allocate_lock() -> lock object
作用都是创建一个 lock object。而详细内容需要看 LockType 部分,现在先放一边。
同时还要这样的一句话:allocate() is an obsolete synonym,也就是说前面的方法是一个过去的语法,是过时的,所以我们使用allocate_lock()就好。
2. exit() / exit_thread()
exit_thread() is an obsolete synonym,同样的,我们使用 exit()就好。
和 raise SystemExit 同义,将退出一个线程,除非进行了异常捕获。
同时,这里也来说说 python中线程退出的方式:
1.调用 thread.exit()之类的退出函数
2.使用python退出进程的标准方法,sys.exit(),连进程都退出了,线程也就消失了。
3.抛出一个 SystemExit 异常
当然,我们有方法可以进行守护线程的设置,不过这个是 threading 模块中的内容。
3. get_ident() -> integer
返回一个非零的整数,惟一地标识当前线程和其他线程同时存在。
这可以用来识别每个线程资源。在一些平台上分配线程的身份是用从1开始的连续数字,但这并不是绝对的,一个线程的身份可能在退出后被另一个线程重用。
4. interrupt_main()
在主线程中抛出 KeyboardInterrupt 异常。
子线程可以使用这个方法来中断主线程。
5. stack_size([size]) -> size
返回创建新线程时使用的线程堆栈的大小。
可以使用 size 参数来指定堆栈大小(以字节为单位),然后再创建相应的线程。而且必须是0(使用平台或配置默认)或一个正整数,其值至少32768(32 k)。如果不支持更改线程堆栈大小,则触发 ThreadError 异常。如果指定的大小是无效的,将触发 ValueError,同时不修改其大小。32 k字节是目前支持最小的堆栈大小值,其目的是为了保证有足够的堆栈空间翻译本身。注意,一些平台可能有特殊的栈大小限制,例如要求最小堆大于32 kb或要求是系统内存页大小的倍数,详情查看该系统平台是说明文档。通常每页 4 kb 是最常见的,在没有说明文档的时候可以尝试使用 4kb 的倍数。
一般用不到,方法实在太底层。
6. start_new(function, args[, kwargs])/start_new_thread(function, args[, kwargs])
start_new() is an obsolete synonym,所以使用 start_new_thread 就行了。
开始一个新的线程,并返回其标识符。线程将调用相应的函数 function 并将 args 元祖中的元素作为位置参数,字典 kwargs 作为关键字参数传入。当函数 return 的时候,线程结束,但是其返还值是忽略的。当函数内部触发未处理的异常时,线程将结束,同时打印堆栈跟踪信息,当然触发 SystemExit 就无痕退出了。
要求一定要有前两个参数。所以,就算我们想要运行的函数不要参数,我们也要传一个空的元组。
可以看出,这里是将一个函数作为一个整体,也就是一个代码块来执行。我们需要做的,就是将我们的业务逻辑封装到一个函数里面去,而函数里面又可以调用其他函数,创建类的实例等等。
用一张图来总结的话:
从某处抠来的代码示例:
def loop1(): print '子线程1开始:', ctime() sleep(4) print '子线程1结束:', ctime() def loop2(): print '子线程2开始:', ctime() sleep(2) print '子线程2结束:', ctime() def main(): print '主线程开始:', ctime() thread.start_new_thread(loop1, ()) thread.start_new_thread(loop2, ()) sleep(6) #为了防止主线程停止,而特意等待了6秒,是函数两个函数执行时间的和:4+2=6 print '主线程结束:', ctime() if __name__ == '__main__': main()
可以看出确实达到了多线程的结果,但是我们为了防止主线程的退出而加上了 sleep(6) ,如何不加上的话,结果是这样的:
主线程并没有等待子线程的执行,而主线程执行完毕后,进程退出,子线程也就消失了。为了防止这样情况,我们让主线程等待着,但是,这里因为我知道两个线程执行的时间总和不会超过6秒,事实上,按照用时最长的函数来算,大于4秒就足够了,但是,当我们改成 sleep(4) 后,结果是这样的:
成功执行完毕。
主线程先退出。
也就是说时间卡的太紧,成功几率还不一定,这要取决于 cpu 是先调度子线程还是主线程,但 cpu 的调度顺序又是不可知的。
但是,通常情况下我们要执行的函数需要的时间是无法确定的,而且更具 cpu 的性能不同,所需要的时间也就不同,这个时候应该怎么办呢?
使用线程锁,也就是使用 allocate_lock()方法。这个方法返回一个 LockType 类型锁对象。下面我们来学习 LockType 又是什么东西。
LockType
其帮助文档依然在 thread 模块中可以看到,去除几个内置方法和重复的方法后,实际上只有3个方法可用,下面我们来看一下。
1. acquire([wait]) -> bool
尝试获取锁对象。
当没有参数的时候,会上加锁,等待另一个线程解锁,如果已经锁定了,则返回True。
当给定参数是,只有当参数的布尔值为真才会加锁,同时返回一个数值反应是否成功加锁。
加锁后若线程运行该代码块时,若不进行解锁操作,将阻塞该线程。阻塞是不可中断的。
2. release()
释放锁,让另一个被阻塞的线程获得线程锁。锁对象必须在锁定状态,但是它不必被同一个线程解锁。
3. locked() -> bool
返回布尔值,判断锁对象是否在锁定状态。
某处抠来的代码示例:
loops = [4, 2] def loop(nloop, nsec, lock): print '子线程', nloop, '开始:', ctime() sleep(nsec) print '子线程', nloop, '结束:', ctime() lock.release() # 解锁 def main(): print '主线程开始:', ctime() locks = [] # 一个用来存放锁对象的列表 nloops = range(len(loops)) for i in nloops: lock = thread.allocate_lock() # 创建一个进程锁对象 lock.acquire() # 尝试获得这个锁对象,并加上锁 locks.append(lock) # 将获得的锁对象放到一个列表中 for i in nloops: thread.start_new_thread(loop, (i, loops[i], locks[i])) # 每次循环启动一个新线程 for i in nloops: while locks[i].locked(): # 查看是否所有线程锁都以释放,否则一直循环,防止主线程停止导致子线程退出 pass print '主线程结束:', ctime() if __name__ == '__main__': main()
因为两个线程是几乎同时开始的,所以开始时打印的那一行显得有点凌乱。
另外,使用 function=loop,之类的关键字传参反而会报错,不要问我为什么知道的。
注意:
我们说过acquire([wait])后,如果不用release()解锁的话,会阻塞当前线程,但是这个例子中,我们为每个函数都开启了一个线程,也就是说,如果我在函数内不释放锁,并不会影响其他线程的执行,只会影响 locked() 函数的返回值。但是,如果我们限制了线程的数量,比如说我的 cpu 是双核4线程的,我希望只开4个线程,在 threading 模块中能够做到。如果我只开了4个线程,但是我的功能函数有很多,我希望他们排队执行,此时若有一个函数锁定了以后老是不释放,那么这个函数所在的线程就会被阻塞掉,其他函数就不能继续在这个线程运行了,只能等待。
这就像你去银行取钱,银行开了4个窗口,本来加上锁是为了保证每个客户的业务都能一次性办完,如果此时有一个人霸着窗口老不走,而他又是加了锁的,那么这个窗口就只能被这个客户使用了,这个窗口后面排队的人就只能干等着了。这就是线程的阻塞了。
转载请注明:爱开源 » python多线程-thread模块