你好,我是悦创。在 Python 中,实现多线程的模块叫作 threading,是 Python 自带的模块。下面我们来了解下使用 threading 实现多线程的方法。
在具体实现之前,我们先来测试一下多线程与当线程裸奔的速度对比,为了更加直观,我这里使用把每种线程代码单独写出来并做对比:
「单线程裸奔:(这也是一个主线程(main thread))」
import time def start(): for i in range(1000000): i += i return # 不使用任何线程(裸着来) def main(): start_time = time.time() for i in range(10): start() print(time.time()-start_time) if __name__ == '__main__': main()
「输出:」
6.553307056427002
「注意:因为每台电脑的性能不一样,所运行的结果也相对不同(请按实际情况分析)」
「接下来我们写一个多线程」
❝我们先创建个字典 「(thread_name_time)」 来存储我们每个线程的名称与对应的时间
❞
import threading,time def start(): for i in range(1000000): i += i return # # 不使用任何线程(裸着来) # def main(): # start_time = time.time() # for i in range(10): # start() # print(time.time()-start_time) # if __name__ == '__main__': # main() def main(): start_time = time.time() thread_name_time = {}# 我们先创建个字典 (thread_name_time) 用来来存储我们每个线程的名称与对应的时间 for i in range(10): # 也就是说,每个线程顺序执行 thread = threading.Thread(target=start)# target=写你要多线程运行的函数,不需要加括号 thread.start()# 上一行开启了线程,这一行是开始运行(也就是开启个 run) thread_name_time[i] = thread # 添加数据到我们的字典当中,这里为什么要用i做key?这是因为这样方便我们join for i in range(10): thread_name_time[i].join() # join() 等待线程执行完毕(也就是说卡在这里,这个线程执行完才会执行下一步) print(time.time()-start_time) if __name__ == '__main__': main()
「输出」
6.2037984102630615
# 6.553307056427002 裸奔 # 6.2037984102630615 单线程顺序执行 # 6.429047107696533 线程并发
❝我们可以看到,速度上的区别不大。
多线程并发不如单线程顺序执行快
这是得不偿失的
造成这种情况的原因就是 GIL
这里是计算密集型,所以不适用
❞
在我们执行加减乘除或者图像处理的时候,都是在从 「CPU」 上面执行才可以。Python 因为 GIL 存在,同一时期肯定只有一个线程在执行,这样这样就是造成我们开是个线程和一个线程没有太大区别的原因。
而我们的网络爬虫大多时候是属于 「IO」 密集与计算机密集
❝BIOS:B:Base、I:Input、O:Output、S:System
❞
也就是你电脑一开机的时候就会启动。
「1. 计算密集型」
❝在上面的时候,我们开启了两个线程,如果这两个线程要同时执行,那同一时期 「CPU」 上只有一个线程在执行。
那从上图可知,那这两个线程就需要频繁的在上下文切换。
Ps:我们这个绿色表示我们这个线程正在执行,红色代表阻塞。
所以,我们可以明显的观察到,线程的上下文切换也是需要消耗资源的(时间-ms)不断的归还和拿取 「GIL」 等,切换上下文。明显造成很大的资源浪费。
❞
「2. IO 密集型」
❝我们现在假设,有个服务器程序(「Socket」)也就是我们新开的一个程序(也就是我们网络爬虫的最底层)开始爬取目标网页了,我们那个网页呢,有两个线程同时运行,我们线程二已经请求成功开始运行了,也就是上图的 (「Thread 2」)绿色一条路过去。
而我们的线程一(「Thread 1」)- 「Datagram」(这里它开启了一个 「UDP」),然后等待数据建立(也就是等待哪些 「HTML、CSS 等数据返回」)也就是说,在 **Ready to receive(recvfrom)**之间都是准备阶段。这样就是有一段时间一直阻塞,而我们的线程二可以一直无停歇也不用切换上下文就一直在运行。「这样的 IO 密集型就有很大的好处」。
❞
还有就是,资源等待,比如有时候我们使用浏览器发起了一个 「Get」 请求,那浏览器图标上面在「转圈圈」的时候就是我们请求资源等待的时间,(也就是图上面的 「Datagram 到 Ready to receive」 )数据建立到数据接收(就是转圈圈的时间)。我们完全就不需要执行它,就让它等待就好。「这个时候让另一个线程去执行就好」
❝换言之就是:第一个线程,我们爬取那个网页转圈圈的时候让另一个线程继续爬取。这样就避免了资源浪费。(把时间都利用起来)
❞
「注意:」 请求资源是不需要 「CPU」 进行计算的,「CPU」 参与是很少的,而我们第一个例子,计算数字的 「for」 循环中,是需要 「CPU」 进行计算的。