摘录 python核心编程
上节介绍的thread模块,是不支持守护线程的。当主线程退出的时候,所有的子线程都将终止,不管他们是否仍在工作。
本节开始,我们开始介绍python的另外多线程模块threading,该模块支持守护线程,其工作方式:守护线程一般是一个等待客户端请求的服务器。如果没有客户端请求,守护线程就是空闲的。如果把一个线程设置为守护线程,就表示这个线程是不重要的,进程退出时不需要等待这个线程执行完成。
如果主线程准备退出的时候,不需要等待某些子线程完成,就可以为这些子线程设置守护线程标记。该标记值为真的时候,标示线程是不重要的,或者说该线程只是用来等待客户端请求而不做其他任何事情。
使用下面的语句:thread.daemon=True 可以将一个线程设置为守护线程。同样的也可以通过这个值来查看线程的守护状态。一个新的子线程会继承父线程的守护标记。整个python程序(也可以称作主线程)将在所有的非守护线程退出之后才退出。
threading模块除了Thread类之外,还包括许多好用的同步机制:
对象 | 描述 |
Thread | 表示一个执行线程的对象 |
Lock | 锁对象 |
RLock | 可重入锁对象,使单一线程可以(再次)获得已持有的锁(递归锁) |
Condition | 条件变量对象,使得一个线程等待另外一个线程满足特定的条件,比如改变状态或者某个数据值 |
Event | 条件变量的通用版本,任意数量的线程等待某个事件的发生,在该事件发生后所有的线程都将被激活 |
Semaphore | 为线程间的有限资源提供一个计数器,如果没有可用资源时会被阻塞 |
BoundedSemaphore | 于Semaphore相似,不过它不允许超过初始值 |
Timer | 于Thread类似,不过它要在运行前等待一定时间 |
Barrier | 创建一个障碍,必须达到指定数量的线程后才可以继续 |
其中,Thread类是threading模块的主要执行对象。
下面是Thread类的属性和方法列表:
属性 | 描述 |
Thread类属性 | |
name | 线程名 |
ident | 线程的标识符 |
daemon | 布尔值,表示这个线程是否是守护线程 |
Thread类方法 | |
__init__(group=None,target=None,name=None,args=(),kwargs={},verbose=None,daemon=None) | 实例化一个线程对象,需要一个可调用的target对象,以及参数args或者kwargs。还可以传递name和group参数。daemon的值将会设定thread.daemon的属性 |
start() | 开始执行该线程 |
run() | 定义线程的方法。(通常开发者应该在子类中重写) |
join(timeout=None) | 直至启动的线程终止之前一直挂起;除非给出了timeout(单位秒),否则一直被阻塞 |
getName() | 返回线程名(该方法已被弃用) |
setName() | 设定线程名(该方法已弃用) |
isAlive | 布尔值,表示这个线程是否还存活(驼峰式命名,python2.6版本开始已被取代) |
isDaemon() | 布尔值,表示是否是守护线程(已经弃用) |
setDaemon(布尔值) | 在线程start()之前调用,把线程的守护标识设定为指定的布尔值(已弃用) |
使用Thread类,可以有多种方法创建线程:
- 创建Thread类的实例,传递一个函数
- 创建Thread类的实例,传递一个可调用的类实例
- 派生Thread类的子类,并创建子类的实例
一般的,我们会采用第一种或者第三种方法。如果需要一个更加符合面向对象的接口时,倾向于选择第三种方法,否则就用第一种方法吧。
第一种方法:创建Thread类,传递一个函数
下面的脚本中,我们先实例化Thread类,并传递一个函数(及其参数),当线程执行的时候,函数也会被执行:
#!/usr/bin/env/ python import threading from time import sleep,ctime #不再把4秒和2秒硬性的编码到不同的函数中,而是使用唯一的loop()函数,并把这些常量放进列表loops中 loops=[4,2] def loop(nloop,nsec): print('开始循环',nloop,'at:',ctime()) sleep(nsec) print('循环',nloop,'结束于:',ctime()) def main(): print('程序开始于:',ctime()) threads=[] nloops=range(len(loops)) for i in nloops: t=threading.Thread(target=loop,args=(i,loops[i])) #循环 实例化2个Thread类,传递函数及其参数,并将线程对象放入一个列表中 threads.append(t) for i in nloops: threads[i].start() #循环 开始线程 for i in nloops: threads[i].join() #循环 join()方法可以让主线程等待所有的线程都执行完毕。 print('任务完成于:',ctime()) if __name__=='__main__': main()
执行结果:
PS C:\Users\WC> python E:\Python3.6.3\workspace\mtsleepC.py 程序开始于: Thu Mar 29 21:35:13 2018 开始循环 0 at: Thu Mar 29 21:35:13 2018 开始循环 1 at: Thu Mar 29 21:35:13 2018 循环 1 结束于: Thu Mar 29 21:35:15 2018 循环 0 结束于: Thu Mar 29 21:35:17 2018 任务完成于: Thu Mar 29 21:35:17 2018
和上节的thread模块相比,不同点在于:实现同样的效果,thread模块需要锁对象,而threading模块的Thread类不需要。实例化Thread(调用Thread())和调用thread.start_new_thread()的最大区别就是新线程不会立即执行!这是一个非常有用的同步功能,尤其在我们不希望线程立即开始执行的时候。
当所有的线程都分配完成之后,通过调用每个线程的start()方法再让他们开始。相比于thread模块的管理一组锁(分配、获取、释放检查锁状态)来说,threading模块的Thread类只需要为每个线程调用join()方法即可。join(timeout=None)方法将等待线程结束,或者是达到指定的timeout时间时。这种锁又称为自旋锁。
最重要的是:join()方法,其实你根本不需要调用它。一旦线程启动,就会一直执行,直到给定的函数完成后退出。如果主线程还有其他事情要做(并不需要等待这些线程完成),可以不调用join()。join()只有在你需要等待线程完成时候才是有用的。
例如上面的脚本中,我们注释掉join()代码:
…… for i in nloops: threads[i].start() #循环 开始线程 ''' for i in nloops: threads[i].join() #循环 join()方法可以让主线程等待所有的线程都执行完毕。 ''' print('任务完成于:',ctime()) if __name__=='__main__': main()
运行结果:
PS C:\Users\WC> python E:\Python3.6.3\workspace\mtsleepC.py 程序开始于: Thu Mar 29 21:55:10 2018 开始循环 0 at: Thu Mar 29 21:55:10 2018 开始循环 1 at: Thu Mar 29 21:55:10 2018 任务完成于: Thu Mar 29 21:55:10 2018 循环 1 结束于: Thu Mar 29 21:55:12 2018 循环 0 结束于: Thu Mar 29 21:55:14 2018
我们发现:主线程的任务比两个循环线程要先执行(任务完成于……在 循环X结束……的前面)
第二种方法:创建Thread类的实例,传递一个可调用的类实例
创建线程时,于传入函数类似的方法是传入一个可调用的类的实例,用于线程执行——这种方法更加接近面向对象的多线程编程。比起一个函数或者从一个函数组中选择而言,这种可调用的类包含一个执行环境,有更好的灵活性。
在上述的mtsleepC.py脚本中添加一个新类ThreadFunc,稍微改动一番,形成mtsleepD.py文件:
#!/usr/bin/env python import threading from time import sleep,ctime loops=[4,2] class ThreadFunc(object): def __init__(self,func,args,name=''): self.name=name self.func = func self.args=args def __call__(self): self.func(*self.args) def loop(nloop,nsec): print('开始循环',nloop,'在:',ctime()) sleep(nsec) print('结束循环',nloop,'于:',ctime()) def main(): print('程序开始于:',ctime()) threads = [] nloops = range(len(loops)) for i in nloops: t = threading.Thread(target=ThreadFunc(loop,(i,loops[i]),loop.__name__)) #传递一个可调用类的实例 threads.append(t) for i in nloops: threads[i].start() #开始所有的线程 for i in nloops: threads[i].join() #等待所有的线程执行完毕 print('任务完成于:',ctime()) if __name__=='__main__': main()
执行结果:
PS C:\Users\WC> python E:\Python3.6.3\workspace\mtsleepD.py 程序开始于: Thu Mar 29 22:30:02 2018 开始循环 0 在: Thu Mar 29 22:30:02 2018 开始循环 1 在: Thu Mar 29 22:30:02 2018 结束循环 1 于: Thu Mar 29 22:30:04 2018 结束循环 0 于: Thu Mar 29 22:30:06 2018 任务完成于: Thu Mar 29 22:30:06 2018
上述脚本中,主要添加了ThreadFunc类,并在实例化Thread对象时,通过传参的形式同时实例化了可调用类ThreadFunc。这里同时完成了两个实例化。
我们研究一下创建ThreadFunc类的思想:我们希望这个类更加通用,而不是局限于loop()函数,为此,添加了一些新的东西,比如这个类保存了函数自身、函数的参数、以及函数名。构造函数__init__()用于设定上述值。当创建新线程的时候,Thread类的代码将调用ThreadFunc对象,此时会调用__call__()这个特殊方法。
(老实说,这种方法显得有些尴尬,并且稍微难以阅读)
第三种方法:派生Thread的子类,并创建子类的实例
和方法二相比,方法三再创建线程时使用子类要相对更容易阅读,下面是mtsleepE.py脚本:
#!/usr/bin/env pyhton import threading from time import sleep,ctime loops=[4,2] class MyThread(threading.Thread): def __init__(self,func,args,name=''): threading.Thread.__init__(self) self.name = name self.func = func self.args = args def run(self): self.func(*self.args) def loop(nloop,nsec): print('开始循环',nloop,'在:',ctime()) sleep(nsec) print('结束循环',nloop,'于:',ctime()) def main(): print('程序开始于:',ctime()) threads = [] nloops = range(len(loops)) for i in nloops: t = MyThread(loop,(i,loops[i]),loop.__name__) threads.append(t) for i in nloops: threads[i].start() for i in nloops: threads[i].join() print('所有的任务完成于:',ctime()) if __name__ =='__main__': main()
运行结果:
PS C:\Users\WC> python E:\Python3.6.3\workspace\mtsleepE.py 程序开始于: Thu Mar 29 22:52:18 2018 开始循环 0 在: Thu Mar 29 22:52:18 2018 开始循环 1 在: Thu Mar 29 22:52:18 2018 结束循环 1 于: Thu Mar 29 22:52:20 2018 结束循环 0 于: Thu Mar 29 22:52:22 2018 所有的任务完成于: Thu Mar 29 22:52:22 2018
比较方法二和方法三,重要的变化在于:MyThread子类的构造函数必须先调用其父类的构造函数;重写run()方法,代替方法二中的__call__()方法。
优化第三种方法:
对MyThread类进行修改,增加一些调试信息的输出,并将该类单独存储为myThread.py的模块(MyThread.py),以便在以后的例子中需要的时候导入这个类。另外,除了简单的调用函数外,还可以将结果保存在实例的属性self.res中,同时增加新的方法getResult()来获取这个值:
#!/usr/bin/env python import threading from time import ctime class MyThread(threading.Thread): def __init__(self,func,args,name=''): threading.Thread.__init__(self) self.func = func self.name = name self.args = args def run(self): print('开始执行',self.name,' 在:',ctime()) self.res = self.func(*self.args) print(self.name,'结束于:',ctime()) def getResult(self): return self.res
下面,我们介绍多线程和单线程执行效果对比的时候,会用到这个MyThread.py模块。
脚本mtfacfib.pt,比较了递归求斐波那契、阶乘和累计函数的执行,分别按照单线程和多线程的方式执行同样的任务:
#!/usr/bin/env python from myThread import MyThread from time import ctime,sleep #斐波那契 def fib(x): sleep(0.005) if x < 2: return 1 return fib(x-1)+fib(x-2) #阶乘 def fac(x): sleep(0.1) if x < 2: return 1 return x*fac(x-1) #累加 def sum(x): sleep(0.1) if x < 2 : return 1 return x + sum(x-1) funcs=[fib,fac,sum] n = 12 def main(): nfuncs = range(len(funcs)) #单线程 print('单线程模式') for i in nfuncs: print('开始',funcs[i].__name__,' 在:',ctime()) print(funcs[i](n)) print(funcs[i].__name__,'结束于:',ctime()) #多线程 print('多线程模式') threads = [] for i in nfuncs : t = MyThread(funcs[i],(n,),funcs[i].__name__) threads.append(t) for i in nfuncs: threads[i].start() for i in nfuncs: threads[i].join() print(threads[i].getResult()) print('所有的任务结束') if __name__ == '__main__': main()
运行结果:
PS C:\Users\WC> python E:\Python3.6.3\workspace\mtfacfib.py 单线程模式 开始 fib 在: Fri Mar 30 14:08:43 2018 233 fib 结束于: Fri Mar 30 14:08:45 2018 开始 fac 在: Fri Mar 30 14:08:45 2018 479001600 fac 结束于: Fri Mar 30 14:08:46 2018 开始 sum 在: Fri Mar 30 14:08:46 2018 78 sum 结束于: Fri Mar 30 14:08:48 2018 多线程模式 开始执行 fib 在: Fri Mar 30 14:08:48 2018 开始执行 fac 在: Fri Mar 30 14:08:48 2018 开始执行 sum 在: Fri Mar 30 14:08:48 2018 fac 结束于: Fri Mar 30 14:08:49 2018 sum 结束于: Fri Mar 30 14:08:49 2018 fib 结束于: Fri Mar 30 14:08:50 2018 233 479001600 78 所有的任务结束
程序中,为了看到多线程如何改善性能的,我们加入了sleep函数用于减慢执行速度。
看到单线程模式中,只是简单的一次调用每个函数,并在函数结束执行的时候立即显示相关的结果;而使用多线程的时候,并不会立刻显示结果,因为我们希望MyThread类越通用越好(有输出和无输出都能执行),我们一直等到所有线程都join之后,再调用getResult()方法显示每个函数的返回值。