Python中的进程(Process),线程(Thread)和协程(Coroutines)的使用讲解
谈这三个问题之前,首先必须清楚一个概念,那就是程序切换(CPU时间的分配)。
我们的任何一个程序,例如开QQ,开浏览器,开游戏,开word编辑器,这些都需要运行在一个操作系统中,如 Windows,Linux, Mac OS。
然后在操作系统中运行的程序,不止一个,而是成百上千个不同功能的程序,如开QQ,开浏览器,开游戏,开word编辑器,键盘驱动,显示器驱动等等。但是CPU等资源是有限的,在这成百上千个程序中,不可能每个程序都占用一个 CPU 来运行,也不可能每个程序只运行一次很短的时间。所以如何来给应用程序分配 CPU,内存等确定数量的资源呢?
通过 程序切换 来实现
指的是:操作系统自动为每个程序分配一些 CPU/内存/磁盘/键盘/显示器 等资源的使用时间,过期后自动切换到下一个程序。当然,被切换的程序,如果没有执行完,它的状态会被保存起来,方便下次轮询到的时候继续执行。实际中,这种切换很快(毫秒级),所以我们感觉不到,好像电脑能自然的同时执行多个软件。
一、进程(Process)“程序切换”的一种方式。
定义:进程,是执行中的计算机程序。也就是说,每个代码在执行的时候,首先本身即是一个进程。
特性:
每个程序,本身首先是一个进程。 运行中每个进程都拥有自己的地址空间、内存、数据栈及其它资源。 操作系统本身自动管理着所有的进程(不需要用户代码干涉),并为这些进程合理分配可以执行时间。 进程可以通过派生新的进程来执行其它任务,不过每个进程还是都拥有自己的内存和数据栈等。 进程间可以通讯(发消息和数据),采用 进程间通信(IPC) 方式。
其实,多个进程可以在不同的 CPU 上运行,互不干扰。同一个CPU上,可以运行多个进程,由操作系统来自动分配时间片。由于进程间资源不能共享,需要进程间通信,来发送数据,接受消息等。
多进程,也称为“并行”。
二、线程(Thread)也“程序切换”的一种方式。
定义:线程,是在进程中执行的代码。是处理器调度的基本单位。
一个进程下可以运行多个线程,这些线程之间共享主进程内申请的操作系统资源。在一个进程中启动多个线程的时候,每个线程按照顺序执行。现在的操作系统中,也支持线程抢占,也就是说其它等待运行的线程,可以通过优先级,信号等方式,将运行的线程挂起,自己先运行。
特性:
线程,必须在一个存在的进程中启动运行。 线程使用进程获得的系统资源,不会像进程那样需要申请CPU等资源。 线程无法给予公平执行时间,它可以被其他线程抢占,而进程按照操作系统的设定分配执行时间。 每个进程中,都可以启动很多个线程。
多线程,也被称为”并发“执行。
三、进程和线程的示例:任务是:从喜马拉雅网站上下载冬吴同学会 所有音频到本地。
先看看下载任务的模型代码:
task_models.py
import time import os import threading import requests from lxml import etree import datetime headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) ' 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36' } def get_audio(): start_url = 'https://www.ximalaya.com/83432108/album/8475135/' response = requests.get(start_url, headers=headers).text num_list = etree.HTML(response).xpath('//p[@class="personal_body"]/@sound_ids')[0].split(',') return num_list def mkdir(title): path = r'D:\ximalayaFM\\' isExists = os.path.exists(os.path.join(path, title)) if not isExists: print(u'创建一个名子叫做{}的文件夹'.format(title)) os.makedirs(os.path.join(path, title)) return True def download(id): # mkdir(u'冬吴同学会') os.chdir(r'D:\ximalayaFM\\冬吴同学会\\') json_url = 'https://www.ximalaya.com/tracks/{}.json'.format(id) html = requests.get(json_url, headers=headers).json() audio_url = html.get('play_path') title = html.get('title') title_ = title + '.m4a' content = requests.get(audio_url).content # 返回的是二进制(常用于图片,音频,视频) with open(title_, 'wb') as f: f.write(content) # 返回下载记过,进程ID,线程ID return '{0}, 下载完毕'.format(title), os.getpid(), threading.currentThread().ident
直接调用函数,同步一个个请求接口下载音频文件结果为:
采用多进程方式下载:
import task_models from multiprocessing import Pool as ProcessPool from multiprocessing.pool import ThreadPool from multiprocessing import freeze_support from multiprocessing import cpu_count import datetime import time import os if __name__ == '__main__': freeze_support() # Windows 平台加上这句,避免RuntimeError cpus = cpu_count() # 得到内核数 print(cpus) start_time = datetime.datetime.now() # 多进程 pool = ProcessPool(processes=5) # 有效控制并发进程或者线程数,不设置默认为内核数(推荐) compute_mode = True # # 多线程 # pool = ThreadPool(processes=5) # compute_mode = False results = {} num_list = task_models.get_audio() for id in num_list: # 获取返回的对象 result_obj = pool.apply_async(task_models.download, args=(id,)) # 把原始的id与返回的对象,放入results字典里 results[(id,)] = result_obj # final_results用于存放id与最终下载结果 find_results = {} while True: for key, val in results.items(): # 如果返回对象ready()返回的结果为False表示还在计算 if not val.ready(): # 了解进程运行状况 print(key, '正在下载中') # 如果返回对象ready()返回的结果不是False表示计算结束,通过get()提取计算返回结果 else: print(key, '下载结果为:', val.get()[0]) time.sleep(1) # 等待1秒 os.system("cls") # windows清空屏幕,准备重新打印 Linux-> os.system('clear') for key, val in results.items(): # 如果没有任何一个返回对象的ready()等于False就重新开始全新的循环 if not val.ready(): break # 如果for循环正常结束(没有break),表示全部计算完成,执行如下else的内容 else: for key, val in results.items(): # 打印最终结果并且放入final_results字典中 print(key, '下载结果为', val.get()[0]) find_results[key] = val.get() print('=' * 30) if compute_mode: print('多进程下载结束!') if not compute_mode: print('多线程下载结束!') print('=' * 30) # 退出整个while循环 break pool.close() pool.join() # 调用join之前,先调用close()函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待进程或者线程执行完 for key, val in find_results.items(): print('id:', key[0]) print('进程ID:{0},线程ID:{1}'.format(val[1], val[2])) print('下载结果为{0}'.format(val[0])) print('=' * 30) # 记录结束的时间 end_time = datetime.datetime.now() # 打印整个过程消耗的时间 print(end_time - start_time)
结果为3分2秒,的确快点,但是肯定不是缩短五倍的效率:
3. 采用多线程方式下载
将代码中的多线程代码放开,将多进程代码注释掉:
pool = ThreadPool(processes=5) compute_mode = False
结果为2分55秒,比多进程快点,可能是任务本身是IO密集型,但是我觉还是很慢,之后会深入研究:
四、进程和线程的区别一个进程中的各个线程与主进程共享相同的资源,与进程间互相独立相比,线程之间信息共享和通信更加容易(都在进程中,并且共享内存等)。
线程一般以并发执行,正是由于这种并发和数据共享机制,使多任务间的协作成为可能。
进程一般以并行执行,这种并行能使得程序能同时在多个CPU上运行;
区别于多个线程只能在进程申请到的的“时间片”内运行(一个CPU内的进程,启动了多个线程,线程调度共享这个进程的可执行时间片),进程可以真正实现程序的“同时”运行(多个CPU同时运行)。
一般来说,在Python中编写并发程序的经验:
计算密集型任务使用多进程 IO密集型(如:网络通讯)任务使用多线程,较少使用多进程。这是由于 IO操作需要独占资源,比如:网络通讯(微观上每次只有一个人说话,宏观上看起来像同时聊天)每次只能有一个人说话文件读写同时只能有一个程序操作(如果两个程序同时给同一个文件写入 ‘a’, ‘b’,那么到底写入文件的哪个呢?) 都需要控制资源每次只能有一个程序在使用,在多线程中,由主进程申请IO资源,多线程逐个执行,哪怕抢占了,也是逐个运行,感觉上“多线程”并发执行了。 如果多进程,除非一个进程结束,否则另外一个完全不能用,显然多进程就“浪费”资源了。
五、协程(Coroutines)协程,也是”程序切换“的一种。
特殊的“线程”,也就是协程
定义:协程,又称微线程。协程是一种用户级的轻量级线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其它协程共享全局数据和其它资源。
协程的主要特色是:协程间是协同调度的,这使得并发量数万以上的时候,协程的性能是远远高于线程。
注意这里也是“并发”,不是“并行”。
协程是用户自己来编写调度逻辑的,对CPU来说,协程其实是单线程,所以CPU不用去考虑怎么调度、切换上下文,这就省去了CPU的切换开销,所以协程在一定程度上又好于多线程。
六、Pyhthon中协程示例Python通过yield提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。
gevent是第三方库,通过greenlet实现协程,其基本思想是:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
笔者只简单尝试了基本使用方式,尝试一下:
代码如下:
from gevent import monkey monkey.patch_all() import gevent import datetime import task_models def download_audio(id): print(task_models.download(id)) gevent.sleep(0) start = datetime.datetime.now() num_list = task_models.get_audio() gevent.joinall( [gevent.spawn(download_audio, id) for id in num_list] ) end = datetime.datetime.now() print('协程下载时间为:{0}'.format(end - start))
其结果为:
下载过程是体验的感觉是这样的,前面感觉什么打印都没有,应该是在请求,然后最后基本很快地一起下载完了,感觉像是把所有资源都请求了,然后大家一起下载,像是线程开的很多的感觉。但笔者对比了下,开70个线程去下载该任务,反而比我们开5个线程下载任务运行时间还要长。看来协程的表现还挺好的。应该还能更好。我会深入研究下去。
高并发最佳的搭配,应该就是多进程+协程了吧!