一.了解多线程
要使用Python的多线程,首先要了解一个概念。GIL(global interpreter lock),翻译过来就是以解释器为单位的全局锁。
用过线程锁的都知道,LOCK就是用来管理住线程,让一个指定的线程先运行,其他的先暂停(等待),避免线程的混乱,尤其是在共用变量的情况下。
GIL也是一样的概念,但是不同的是:
1.你可以想成他是解释器控制的
2.线程的指定是随机的
3.每个线程acquire运行机会后,可运行的内容很少(因此线程间的切换超级快)
因此,多线程看起来好像是多个线程一起运行,实际上也是大家轮流。比起单线程,他还多了一个线程切换的开销,因此执行效率上,很多时候是不如单线程的。
那多线程有什么用的,我认为在以下及方面,还是用多线程比较有效率:
1.单线程只能根据顺序一行行从上往下执行。那你想一遍操作前台,一边后台记录日志的时候,就需要用到多线程。或者你想一边运行脚本,一边等待指令来停止脚步的时候,也需要多线程。
2.对于IO密集型的线程,多线程则可以提高效率。例如下面的例子,线程向服务器发送信息,服务器接收后等待5s再返回给线程。如果用单线程的方式,则一共要等待10s
from client import send_and_receive def main(): start_time = time.time() for i in [40080,40081]: t = Thread(target=send_and_receive, args=(i,)) t.start() t.join() end_time = time.time() print("Total time: {}".format(end_time - start_time)) if __name__ == '__main__': main()
而使用多线程的方式,则一共只要等待5左右s,因为这个等待是同步的
from client import send_and_receive def main(): start_time = time.time() for i in [40080,40081]: t = Thread(target=send_and_receive,args=(i,)) t.start() t.join() end_time = time.time() print("Total time: {}".format(end_time - start_time)) if __name__ == '__main__': main()
****************************client**********************
import socket def send_and_receive(port): s_obj=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s_obj.connect(('localhost',port)) s_obj.send('x') while True: buf=s_obj.recv(1024) if buf!='': return buf
****************************server**********************
#为更准确的统计时间,服务器写了2个不同端口的,而不是在服务器中使用线程
import socket import time s_obj=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s_obj.bind(('localhost',40080)) s_obj.listen(5) while True: conn,ipaddr=s_obj.accept() while True: if conn.recv(1024)!='': time.sleep(5) print 'send back' conn.send('back') conn.close() break
二.多线程的使用和管理
1.创建
常用的多线程库有两个:threading和Thread
一般我们都推荐用threading,因为Thread提供的线程管理资源较少,并且不支持守护线程,主线程结束后子线程强制结束。
使用threading创建线程的方式一般有2个:
直接使用threading.Thread(target=func,args=*args)
继承线程类:
class mythread(threading.Tread): def __init__(self,*args): threading.Thread.__init__(self)
self.args=args
2.启动
启动一般没有什么好说的。就是.start
不过有时候容易分不清start和run
run其实只是线程的一个方法,start才是启用一个线程。
如果你用threading.Thread.run()可以运行,但是这个时候其实是单线程进行运行
3.结束
主线程和子线程的结束关系如下图所示:
1.默认情况下,子线程不是守护线程,主线程结束后,子线程会继续运行
2.让主线程、子线程一起结束的方式有2个:
a)将子线程设置为守护线程(setDaemon(True)),这个时候是主线程结束的时候,子线程被强制结束
b)在主线程中加入join(),这个时候是主线程等待子线程结束后再一起结束。join在这里起的是一个阻塞的作用。也就是主线程中运行的步骤是,join()→子线程运行完成→主线程中join()后面的的内容
def son(): print 's' time.sleep(2) print 'ss' if __name__ == '__main__': print 't' t=threading.Thread(target=son) # t.setDaemon(True) t.start() t.join() time.sleep(3) print 'tt'
例如上面的例子,son执行完 print 'ss' 后,主线程再等待3s然后print 'tt'
在实际的使用中,控制线程的结束不是一件难事,容易出错的是,是否能理清所有的线程的开启和关闭的逻辑,能让线程在使用的逻辑中能正确的关闭,既不拖延又不被强制结束。
4.锁
虽然整个Python中有一个GIL的大锁,但是我们也从上面的介绍看出,这个锁的使用是不受我们控制的,并且是随机的。
那如果多个线程使用一个公共变量的时候,要怎么让线程的使用能够不产生混乱呢。我们可以使用线程锁
lock=threading.Lock()
当运行到lock.acquire()时,GIL不再快速的随机切换线程了,解释器会一直'单线程'地运行lock.acquire()后面的内容,直到执行lock.release(),再恢复GIL的快速切换功能
5.线程池
线程的大致生命过程是:
T1:线程创建
T2:线程运行
T3:线程销毁
T1+T3属于线程的固定开销,当线程使用的较多的情况下,线程的固定开销显得不容忽视。
线程池是一个线程的预创建机制。线程池会预先创建满载的线程数(例如pool(5)就预先创建5个),然后等到需要用到线程的时候,对线程池中的线程进行分配。当需要的线程数大于线程池的数量的时候,就会有一个排队等待的过程。
我认为这个机制的好处是,当需要的线程数比较多的时候,可以节省T1+T3的开销(线程池中的线程重复利用),另外从系统的性能上说,线程池的等待机制能比较好的控制系统对线程的负荷
三、多进程
多进程我们一般推荐使用multiprocessing模块,进程的大多数使用原理同线程是一样的(类似的)。但是多进程在使用上几个比较大的不同是:
1.Windows系统下,启用进程模块需要在if '__name__'=='__main__':的情况下使用,否则会报错。
2.进程启用时,是对整个文件复制了一份进行运行,因此multiprocessing.process().start()之前的语句会进行重复运行,这个是在写程序的情况下需要注意的地方
3.当进程的主程序结束时,相当于一个解释器就停止工作了。这个时候,进程里面的所有资源都是释放掉的,因此在进程中使用线程的时候要注意线程因进程的结束而强制结束的风险(无论是否设置守护线程)