当前位置: 移动技术网 > IT编程>脚本编程>Python > Python中的进程(Process),线程(Thread)和协程(Coroutines)的使用讲解

Python中的进程(Process),线程(Thread)和协程(Coroutines)的使用讲解

2017年12月23日  | 移动技术网IT编程  | 我要评论

qq非主流英文网名,域名转入,詹姆斯十大扣篮

谈这三个问题之前,首先必须清楚一个概念,那就是程序切换(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个线程下载任务运行时间还要长。看来协程的表现还挺好的。应该还能更好。我会深入研究下去。

高并发最佳的搭配,应该就是多进程+协程了吧!

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网