欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-07-08 13:59:59
谈这三个问题之前,首先必须清楚一个概念,那就是程序切换(CPU时间的分配)。 我们的任何一个程序,例如开QQ,开浏览器,开游戏,开word编辑器,这些都需要运行在一个操作系统中,...

谈这三个问题之前,首先必须清楚一个概念,那就是程序切换(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

直接调用函数,同步一个个请求接口下载音频文件结果为:

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

采用多进程方式下载:

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秒,的确快点,但是肯定不是缩短五倍的效率:

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

3. 采用多线程方式下载

将代码中的多线程代码放开,将多进程代码注释掉:

    pool = ThreadPool(processes=5)
    compute_mode = False

结果为2分55秒,比多进程快点,可能是任务本身是IO密集型,但是我觉还是很慢,之后会深入研究:

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

四、进程和线程的区别

一个进程中的各个线程与主进程共享相同的资源,与进程间互相独立相比,线程之间信息共享和通信更加容易(都在进程中,并且共享内存等)。

线程一般以并发执行,正是由于这种并发和数据共享机制,使多任务间的协作成为可能。

进程一般以并行执行,这种并行能使得程序能同时在多个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))

其结果为:

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

下载过程是体验的感觉是这样的,前面感觉什么打印都没有,应该是在请求,然后最后基本很快地一起下载完了,感觉像是把所有资源都请求了,然后大家一起下载,像是线程开的很多的感觉。但笔者对比了下,开70个线程去下载该任务,反而比我们开5个线程下载任务运行时间还要长。看来协程的表现还挺好的。应该还能更好。我会深入研究下去。

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