Python学习之==>多线程多进程

一、线程&进程

  对于操作系统来说,一个任务就是一个进程(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密集型任务(计算/排序)

猜你喜欢

转载自www.cnblogs.com/L-Test/p/9283100.html