一、线程&进程
对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。进程是很多资源的集合。多进程多用于CPU密集型任务,例如:排序、计算,都是消耗CPU的。
有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。多线程多用于处理IO密集型任务频繁写入读出,cpu负责调度,消耗的是磁盘空间。
由于每个进程至少要干一件事,所以,一个进程至少有一个线程。当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。线程是最小的执行单元,而进程由至少一个线程组成。
我们想运行的速度快一点的话,就得使用多线程或者多进程,在python里面,多线程被很多人诟病,为什么呢,因为Python的解释器使用了GIL的一个叫全局解释器锁,它不能利用多核CPU,只能运行在一个cpu上面,但是你在运行程序的时候,看起来好像还是在一起运行的,是因为操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。这个叫做上下文切换。
二、多线程(threading.Thread(target=方法))
Python中的多线程使用threading模块,下面是一个简单的多线程
1 import threading,time 2 def run(): # 定义每个线程需要运行的函数 3 time.sleep(3) 4 print('呵呵呵') 5 6 # 串行 7 for i in range(5): # 串行,需要运行15秒 8 run() 9 10 # 多线程: 11 for j in range(5): # 并行:运行3秒 12 t = threading.Thread(target=run) # 实例化了一个线程 13 t.start()
下面再举一个例子来对比单线程和多线程的运行速度:
1 import requests,time,threading 2 # 定义需要下载的网页字典 3 urls = { 4 'besttest':'http://www.besttest.cn', 5 'niuniu':'http://www.nnzhp.cn', 6 'dsx':'http://www.imdsx.cn', 7 'cc':'http://www.cc-na.cn', 8 'alin':'http://www.limlhome.cn' 9 } 10 # 下载网页并保存成html文件 11 # 子线程运行的函数,如果里面有返回值的话,是不能获取到的 12 # 只能在函数外面定义一个list或者字典来存每次处理的结果 13 data = {} 14 def down_html(file_name,url): 15 start_time = time.time() 16 res = requests.get(url).content # content就是返回的二进制文件内容 17 open(file_name+'.html','wb').write(res) 18 end_time = time.time() 19 run_time = end_time - start_time 20 data[url] = run_time 21 22 # 串行 23 start_time = time.time() # 记录开始执行时间 24 for k,v in urls.items(): 25 down_html(k,v) 26 end_time = time.time() # 记录执行结束时间 27 run_time = end_time - start_time 28 print(data) 29 print('串行下载总共花了%s秒'%run_time) 30 31 # 并行 32 start_time = time.time() 33 for k,v in urls.items(): 34 t = threading.Thread(target=down_html,args=(k,v)) # 多线程的函数如果传参的话,必须得用args 35 t.start() 36 end_time = time.time() 37 run_time = end_time-start_time 38 print(data) 39 print('并行下载总共花了%s秒'%run_time)
串行运行结果:
并行运行结果:
从以上运行结果可以看出,并行下载的时间远短于串行。但是仔细观察会发现:并行运行时,打印出运行时间后,程序并没有结束运行,而是等待了一段时间后才结束运行。实际上并行运行时,打印的是主线程运行的时间,主线程只是负责调起5个子线程去执行下载网页内容,调起子线程以后主线程就运行完成了,所以执行时间才特别短,主线程结束后子线程并没有结束。所以0.015s这个时间是主线程运行的时间,而不是并行下载的时间。如果想看到并行下载的时间,就需要引入线程等待。
三、线程等待(t.join())
1 import requests,time,threading 2 # 定义需要下载的网页字典 3 urls = { 4 'besttest':'http://www.besttest.cn', 5 'niuniu':'http://www.nnzhp.cn', 6 'dsx':'http://www.imdsx.cn', 7 'cc':'http://www.cc-na.cn', 8 'alin':'http://www.limlhome.cn' 9 } 10 # 下载网页并保存成html文件 11 # 子线程运行的函数,如果里面有返回值的话,是不能获取到的 12 # 只能在函数外面定义一个list或者字典来存每次处理的结果 13 data = {} 14 def down_html(file_name,url): 15 start_time = time.time() 16 res = requests.get(url).content # content就是返回的二进制文件内容 17 open(file_name+'.html','wb').write(res) 18 end_time = time.time() 19 run_time = end_time - start_time 20 data[url] = run_time 21 22 # 串行 23 start_time = time.time() # 记录开始执行时间 24 for k,v in urls.items(): 25 down_html(k,v) 26 end_time = time.time() # 记录执行结束时间 27 run_time = end_time - start_time 28 print(data) 29 print('串行下载总共花了%s秒'%run_time) 30 31 # 多线程 32 start_time = time.time() 33 threads = [] # 存放启动的5个子线程 34 for k,v in urls.items(): 35 # 多线程的函数如果传参的话,必须得用args 36 t = threading.Thread(target=down_html,args=(k,v)) 37 t.start() 38 threads.append(t) 39 for t in threads: # 主线程循环等待5个子线程执行结束 40 t.join() # 循环等待 41 print(data) # 通过函数前面定义的data字典获取每个线程执行的时间 42 end_time = time.time() 43 run_time = end_time - start_time 44 print('并行下载总共花了%s秒'%run_time)
多线程运行结果:
从执行结果来看,总运行时间只是稍稍大于最大的下载网页的时间(主线程调起子线程也需要一点时间),符合多线程的目的。有了线程等待,主线程就会等到子线程全部执行结束后再结束,这样统计出的才是真正的并行下载时间。
看到这里,我们还需要回答一个问题:为什么Python的多线程不能利用多核CPU,但是在写代码的时候,多线程的确在并发,而且还比单线程快
电脑cpu有几核,那么只能同时运行几个线程。但是python的多线程,只能利用一个cpu的核心。因为Python的解释器使用了GIL的一个叫全局解释器锁,它不能利用多核CPU,只能运行在一个cpu上面,但是运行程序的时候,看起来好像还是在一起运行的,是因为操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。这个叫做上下文切换。
Python只有一个GIL,运行python时,就要拿到这个锁才能执行,在遇到I/O 操作时会释放这把锁。如果是纯计算的程序,没有 I/O 操作,解释器会每隔100次操作就释放这把锁,让别的线程有机会 执行(这个次数可以通sys.setcheckinterval来调整)同一时间只会有一个获得GIL线程在跑,其他线程都处于等待状态。
1、如果是CPU密集型代码(循环、计算等),由于计算工作量多和大,计算很快就会达到100,然后触发GIL的释放与在竞争,多个线程来回切换损耗资源,所以在多线程遇到CPU密集型代码时,单线程会比较快;
2、如果是I\O密集型代码(文件处理、网络爬虫),开启多线程实际上是并发(不是并行),IO操作会进行IO等待,线程A等待时,自动切换到线程B,这样就提升了效率。
四、线程锁
Python2种多个线程同时修改一个数据的时候,可能会把数据覆盖,所以需要加线程锁(threading.lock())。但Python3中不需要,默认会自动帮你加锁。
1 import threading,time 2 num = 1 3 lock = threading.Lock() # 实例化一把锁 4 def run(): 5 time.sleep(1) 6 global num 7 lock.acquire() # 加锁 8 num = num + 1 9 lock.release() # 解锁 10 ts = [] 11 for i in range(50): 12 t = threading.Thread(target=run) 13 t.start() 14 ts.append(t) 15 [t.join() for t in ts] 16 print(num)
五、守护线程(setDaemon(True))
所谓守护线程的意思就是:只要主线程结束,那么子线程立即结束,不管子线程有没有运行完成。
1 import threading,time 2 def run(): 3 time.sleep(3) 4 print('哈哈哈') 5 6 for i in range(50): 7 t = threading.Thread(target=run) 8 t.setDaemon(True) # 把子线程设置成为守护线程 9 t.start() 10 print('Done,运行完成') 11 time.sleep(3)
六、多进程
一个简单的多进程,multiprocessing.Process(target=run,args=(6,))
1 import multiprocessing,threading 2 def my(): 3 print('哈哈哈') 4 5 def run(num): 6 for i in range(num): 7 t = threading.Thread(target=my) 8 t.start() 9 # 总共启动5个进程,每个进程下面启动6个线程,函数my()执行30次 10 if __name__ == '__main__': 11 process = [] 12 for i in range(5): 13 # args只有一个参数一定后面要加逗号 14 p = multiprocessing.Process(target=run,args=(6,)) # 启动一个进程 15 p.start() 16 process.append(p) 17 [p.join() for p in process] # 与线程用法一致
七、多线程、多进程总结
1、多线程:
多用于IO密集型行为(上传/下载)
2、多进程
多用于CPU密集型任务(计算/排序)