<路径 clip-rule="evenodd" d="M33.377 4.574a3.508 3.508 0 0 0-2.633-1.126c-1 0-1.993.67-2.604 1.334l.002-1.24-1.867-.002-.02 10.17v.133l1.877.002.008-3.18c.567.611 1.464.97 2.462.973 1.099 0 2.022-.377 2.747-1.117.73-.745 1.1-1.796 1.103-3.002.003-1.232-.358-2.222-1.075-2.945Zm-3.082.55c.637 0 1.176.23 1.602.683.438.438.663 1.012.66 1.707-.003.7-.22 1.33-.668 1.787-.428.438-.964.661-1.601.661-.627 0-1.15-.22-1.6-.666-.445-.46-.662-1.086-.662-1.789.003-.695.227-1.27.668-1.708a2.13 2.13 0 0 1 1.596-.675h.005Zm5.109-.067-.008 4.291c-.002.926.263 1.587.784 1.963.325.235.738.354 1.228.354.376 0 .967-.146.967-.146l-.168-1.564s-.43.133-.64-.01c-.198-.136-.296-.428-.296-.866l.008-4.022 1.738.002.002-1.492-1.738-.002.005-2.144-1.874-.002-.005 2.143-1.573-.002 1.57 1.497ZM20.016 1.305h-9.245l-.002 1.777h3.695l-.016 8.295v.164l1.955.002-.008-8.459 3.621-.002V1.305Z" fill="#262D3D" fill-rule="evenodd"><路径 clip-rule="evenodd" d="M10.06 5.844 7.277 3.166 4.015.03 2.609 1.374l2.056 1.978-4.51 4.313 6.065 5.831 1.387-1.327-2.073-1.994 4.526-4.331ZM4.274 8.7a.211.211 0 0 1-.124 0c-.04-.013-.074-.03-.15-.102l-.817-.787c-.072-.069-.092-.104-.105-.143a.187.187 0 0 1 0-.12c.013-.039.03-.07.105-.143L5.76 4.938c.072-.07.108-.09.15-.099a.21.21 0 0 1 .123 0c.041.012.075.03.15.101L7 5.727c.072.07.093.104.103.144.013.04.013.08 0 .119-.013.04-.03.072-.106.143L4.422 8.601a.325.325 0 0 1-.147.099Z" fill="#204ECF" fill-rule="evenodd"><路径 clip-rule="evenodd" d="M24.354 4.622a3.94 3.94 0 0 0-2.876-1.149 4.1 4.1 0 0 0-2.829 1.084c-.804.725-1.214 1.733-1.217 2.992-.002 1.26.405 2.267 1.207 2.995a4.114 4.114 0 0 0 2.832 1.094c.04.002.082.002.123.002a3.967 3.967 0 0 0 2.75-1.138c.538-.532 1.183-1.473 1.186-2.938.002-1.465-.637-2.408-1.176-2.942Zm-.59 2.94c-.003.73-.228 1.334-.671 1.794-.441.458-.99.69-1.633.69a2.166 2.166 0 0 1-1.614-.697c-.43-.45-.65-1.057-.65-1.797s.222-1.344.655-1.795a2.17 2.17 0 0 1 1.617-.69c.64 0 1.189.235 1.63.698.443.46.668 1.064.665 1.797ZM41.15 6.324c0-.458.25-1.465 1.632-1.465.49 0 .768.159 1.003.347.227.18.34.626.34.994v.174l-2.282.341C40.035 6.98 39 7.913 38.993 9.28c-.002.708.266 1.314.777 1.76.503.438 1.191.67 2.004.673 1.023 0 1.792-.354 2.341-1.084.003.31.003.621.003.91h1.903l.013-5.246c.002-.856-.289-1.685-.864-2.14-.567-.449-1.31-.679-2.386-.681h-.015c-.82 0-1.69.208-2.274.695-.689.572-1.027 1.478-1.027 2.178l1.682-.02Zm.864 3.814c-.676-.002-1.115-.371-1.112-.938.003-.589.43-.933 1.346-1.081l1.875-.305v.017c-.005 1.36-.87 2.307-2.102 2.307h-.008Zm4.917-8.712-.018 10.058v.044l1.684.005.018-10.06v-.045l-1.684-.002Zm2.654 9.491c0-.173.062-.322.19-.445a.645.645 0 0 1 .462-.186c.18 0 .338.062.465.186a.596.596 0 0 1 .193.445.583.583 0 0 1-.193.443.644.644 0 0 1-.465.183.634.634 0 0 1-.461-.183.59.59 0 0 1-.191-.443Zm.108 0c0 .146.052.273.158.376a.54.54 0 0 0 .389.154.539.539 0 0 0 .547-.53.498.498 0 0 0-.16-.373.531.531 0 0 0-.387-.156.531.531 0 0 0-.387.155.497.497 0 0 0-.16.374Zm.702.344-.176-.3h-.118v.3h-.109v-.688h.292c.144 0 .23.082.23.196 0 .096-.076.168-.176.188l.178.304h-.121Zm-.294-.596v.21h.167c.093 0 .14-.034.14-.104 0-.072-.047-.106-.14-.106h-.167Z" fill="#262D3D" fill-rule="evenodd">作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

马库斯·麦柯迪

Marcus是一个有才华的程序员,擅长后端开发. 然而,他很适应全栈开发.

专业知识

以前在

海军海上系统司令部

批评Python的讨论经常讨论如何难以使用Python进行多线程工作, 指向所谓的全局解释器锁(亲切地称为 吉尔),它可以防止多个Python代码线程同时运行. 因此, Python多线程模块的行为并不完全像您所期望的那样,如果您不是 Python开发人员 而你来自其他语言,如c++或Java. 必须明确的是,仍然可以用Python编写并发或并行运行的代码,并在最终性能上产生明显的差异, 只要考虑到某些事情. 如果你还没有读过,我建议你看一下平等古兰经 关于Ruby中的并发和并行的文章 在Toptal工程博客上.

演示Python中的并发性, 我们将编写一个小脚本,从Imgur下载最受欢迎的图片. 我们将从一个按顺序下载图像或一次下载一个图像的版本开始. 作为先决条件,你必须注册 Imgur上的一个应用程序. 如果您还没有Imgur帐户,请先创建一个.

这些Python多线程示例中的脚本已在Python 3中进行了测试.6.4. 做了一些改变, 它们也应该与Python 2一起运行- urllib是这两个Python版本之间变化最大的地方.

Python多线程入门

让我们从创建Python模块开始,命名为 下载.py. 该文件将包含获取和下载图像列表所需的所有函数. 我们将这些功能分成三个独立的函数:

  • get_links
  • 下载_link
  • setup_下载_dir

第三个函数, setup_下载_dir,如果下载目标目录不存在,则使用该参数创建下载目标目录.

Imgur的API需要HTTP请求来承受 授权 头与客户端ID. 您可以从您在Imgur上注册的应用程序的仪表板中找到这个客户端ID, 响应将是JSON编码. 我们可以使用Python的标准JSON库来解码它. 下载图像是一个更简单的任务, 因为您所要做的就是通过URL获取图像并将其写入文件.

脚本看起来是这样的:

进口json
导入日志
进口操作系统
从路径lib导入路径
从urllib.请求导入ur循环en

日志记录.getlog (__name__)

类型= {'图像/jpeg', '图像/png'}


def get_links (client_id):
    headers = {'授权': 'Client-ID {}'.格式(client_id)}
    req =请求('http://api.imgur.', headers=headers, method='GET')
    以urlopen(req)为例:
        Data = json.负载(分别地.read ().解码(utf - 8))
    如果item中有type,而type中有item['type'],则返回data['data']中的item['link']


Def 下载_link(目录,链接):
    Download_路径 = directory / OS.路径.basename(链接)
    使用urlopen(链接)作为图像,下载路径.打开('wb')作为f:
        f.写(图片.read ())
    日志记录器.info('已下载%s',链接)


def setup_下载_dir ():
    下载_dir = Path('图片')
    如果不是,下载_dir.存在():
        下载_dir.mkdir ()
    返回下载_dir

下一个, 我们需要编写一个模块,使用这些函数来下载图像, 一个接一个. 我们给它命名 单.py. 这将包含我们第一个简单版本的Imgur图像下载器的主要功能. 该模块将在环境变量中检索Imgur客户端ID IMGUR_CLIENT_ID. 它将调用 setup_下载_dir 创建下载目标目录. 最后,它将使用 get_links 函数,过滤掉所有GIF和相册的url,然后使用 下载_link 下载并将每个映像保存到磁盘. 下面是 单.py 看起来像:

导入日志
进口操作系统
从时间导入时间

从下载导入setup_下载_dir, get_links, 下载_link

日志记录.basicConfig(=日志级别.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
日志记录.getlog (__name__)

def main ():
    Ts = time()
    Client_id = OS.采用“IMGUR_CLIENT_ID”)
    如果不是client_id:
        无法找到IMGUR_CLIENT_ID环境变量!")
    Download_dir = setup_下载_dir()
    链接= get_links(client_id)
    连结中的连结:
        下载_link (下载_dir链接)
    日志记录.info('用时%s秒',time() - ts)

如果__name__ == '__main__':
    main ()

在我的笔记本电脑上,这个脚本花了19分钟.4秒下载91张图片. 请注意,这些数字可能会根据您所使用的网络而有所不同. 19.4秒不是很长,但是如果我们想下载更多的图片呢? 也许是900张图片,而不是90张. 平均值为0.每张图片2秒,900张图片大约需要3分钟. 拍摄9000张照片需要30分钟. 好消息是,通过引入并发性或并行性,我们可以显著加快这一速度.

所有后续的代码示例将只显示新的和特定于这些示例的import语句. 为方便起见,所有这些Python脚本都可以在 这个GitHub存储库.

Python中的并行性和并发性:多线程示例

线程是在Python中实现并行性和并发性的最著名的方法之一. 线程化通常是操作系统提供的一个特性. 线程比进程轻,并且共享相同的内存空间.

Python多线程内存模型

在这个Python多线程示例中,我们将编写一个新模块来替换 单.py. 该模块将创建一个包含八个线程的池, 包括主线程在内总共有9个线程. 我选择了8个工作线程,因为我的计算机有8个CPU核心,每个核心一个工作线程似乎是一次运行多少个线程的好数字. 在实践中, 这个数字的选择要根据其他因素仔细得多, 例如在同一台机器上运行的其他应用程序和服务.

这和前一个几乎是一样的,除了我们现在有一个新的类, DownloadWorker它是蟒蛇的后代 线程 class. run方法已被重写,它将运行一个无限循环. 在每次迭代中,它调用 自我.队列.get () 尝试从线程安全队列中获取URL. 它会阻塞,直到队列中有一个项目供工人处理. 一旦工作线程从队列中接收到一个项目,它就会调用该项目 下载_link 方法,该方法在前面的脚本中用于将图像下载到图片目录. 下载完成后,工人向队列发出任务完成的信号. 这一点非常重要,因为队列会跟踪进入队列的任务数量. 呼唤 队列.加入() 如果工作线程没有发出完成任务的信号,会永远阻塞主线程吗.

导入日志
进口操作系统
从队列导入队列
从线程导入线程
从时间导入时间

从下载导入setup_下载_dir, get_links, 下载_link


日志记录.basicConfig(=日志级别.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

日志记录.getlog (__name__)


类DownloadWorker(线程):

    Def __init__(自我, 队列):
        线程.__init__(自我)
        自我.队列=排队

    def运行(自我):
        而真正的:
            #从队列中获取工作并扩展元组
            目录,link = 自我.队列.get ()
            试一试:
                下载_link(目录,链接)
            最后:
                自我.队列.task_done ()


def main ():
    Ts = time()
    Client_id = OS.采用“IMGUR_CLIENT_ID”)
    如果不是client_id:
        无法找到IMGUR_CLIENT_ID环境变量!")
    Download_dir = setup_下载_dir()
    链接= get_links(client_id)
    #创建一个队列与工作线程通信
    队列=排队 ()
    创建8个工作线程
    对于范围(8)内的x:
        工人 = DownloadWorker(队列)
        #设置daemon为True将使主线程退出,即使工作线程阻塞
        工人.daemon = True
        工人.start ()
    #将任务以元组的形式放入队列
    连结中的连结:
        日志记录器.信息(“排队{}”.格式(链接))
        队列.((下载_dir链接))
    导致主线程等待队列处理完所有任务
    队列.加入()
    日志记录.info('占用%s', time() - s)

如果__name__ == '__main__':
    main ()

在之前使用的同一台机器上运行这个Python多线程示例脚本的下载时间为4.1秒! 这是4.比之前的例子快7倍. 这要快得多, 值得一提的是,由于吉尔,在整个过程中一次只执行一个线程. 因此,这段代码是并发的,但不是并行的. 它仍然更快的原因是因为这是一个IO绑定任务. 处理器在下载这些图像时几乎不会出汗, 大部分时间都花在等待网络上. 这就是为什么Python多线程可以提供很大的速度提升. 当其中一个线程准备好执行某些工作时,处理器可以在线程之间切换. 在Python或任何其他带有吉尔的解释性语言中使用线程模块实际上会导致性能降低. 如果您的代码正在执行CPU绑定任务,例如解压缩gzip文件,则使用 线程 模块将导致较慢的执行时间. 对于CPU受限的任务和真正的并行执行,我们可以使用多处理模块.

事实上的 参考Python实现- cpython -有一个吉尔,这并不是所有Python实现都是如此. 例如,IronPython,一个使用 .. NET框架没有吉尔,基于java的实现Jython也没有吉尔. 您可以找到工作Python实现的列表 在这里.

Python 多处理:生成多个进程

Python多处理模块比线程模块更容易使用, 因为我们不需要像Python多线程示例那样添加一个类. 我们需要做的唯一更改是在主函数中.

Python多处理教程:模块

为了使用多个进程,我们创建了一个多进程 . 使用它提供的map方法, 我们将把url列表传递给池, 这将依次产生8个新进程,并使用每个进程并行下载图像. 这是真正的并行,但它是有代价的. 脚本的整个内存被复制到生成的每个子进程中. 在这个简单的例子中, 这没什么大不了的, 但它很容易成为重要程序的严重开销.

导入日志
进口操作系统
从functools导入partial
从多处理.pool import pool
从时间导入时间

从下载导入setup_下载_dir, get_links, 下载_link


日志记录.basicConfig(=日志级别.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
日志记录.getlog(请求).setLevel(日志记录.关键)
日志记录.getlog (__name__)


def main ():
    Ts = time()
    Client_id = OS.采用“IMGUR_CLIENT_ID”)
    如果不是client_id:
        无法找到IMGUR_CLIENT_ID环境变量!")
    Download_dir = setup_下载_dir()
    链接= get_links(client_id)
    下载= partial(下载_link, 下载_dir)
    设池(4)为p:
        p.地图(下载链接)
    日志记录.info('用时%s秒',time() - ts)


如果__name__ == '__main__':
    main ()

分配给多个工作人员

而Python多线程和多处理模块对于在个人计算机上运行的脚本来说是非常好的, 如果您希望在另一台机器上完成工作,应该怎么做, 或者您需要扩展到一台机器上的CPU无法处理的范围? 一个很好的用例是web应用程序的长时间后端任务. 如果您有一些长时间运行的任务, 您不希望在同一台机器上启动一堆需要运行其余应用程序代码的子进程或线程. 这将降低应用程序对所有用户的性能. 如果能够在另一台机器或许多其他机器上运行这些作业,那就太棒了.

用于此任务的一个很好的Python库是 RQ,一个非常简单但功能强大的库. 首先使用库将函数及其参数排队. 这 泡菜 函数调用表示,然后将其附加到 复述, 列表. 将作业加入队列是第一步,但还不会做任何事情. 我们还需要至少一个工人来监听该作业队列.

RQ Python队列库的模型

第一步是在计算机上安装并运行复述,服务器, 或者访问正在运行的复述,服务器. 在此之后,只需要对现有代码进行一些小的更改. 我们首先创建一个RQ 队列的实例,并将复述,服务器的实例传递给它 redis-py图书馆. 然后,不要只是调用我们的 下载_link 方法,我们调用 q.En队列 (下载_link, 下载_dir, link). en队列方法接受一个函数作为其第一个参数, 然后,在实际执行作业时,将任何其他参数或关键字参数传递给该函数.

我们需要做的最后一步是启动一些工人. RQ提供了一个方便的脚本来在默认队列上运行工人. 你就跑 rq工人 在终端窗口中,它将启动一个监听默认队列的工作线程. 请确保您当前的工作目录与脚本所在的目录相同. 如果您想要收听不同的队列,您可以运行 rq工人 队列_name 它会监听那个命名队列. RQ的伟大之处在于,只要你能连接到复述,, you can run as many 工人s as you like on as many different machines as you like; t在这里fore, 随着应用程序的增长,扩展是非常容易的. 以下是RQ版本的源代码:

导入日志
进口操作系统

导入redis

从rq导入队列

从下载导入setup_下载_dir, get_links, 下载_link


日志记录.basicConfig(=日志级别.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
日志记录.getlog(请求).setLevel(日志记录.关键)
日志记录.getlog (__name__)


def main ():
    Client_id = OS.采用“IMGUR_CLIENT_ID”)
    如果不是client_id:
        无法找到IMGUR_CLIENT_ID环境变量!")
    Download_dir = setup_下载_dir()
    链接= get_links(client_id)
    q =队列(连接=复述,(主机='localhost',端口=6379))
    连结中的连结:
        q.En队列 (下载_link, 下载_dir, link)

如果__name__ == '__main__':
    main ()

然而,RQ并不是唯一的Python作业队列解决方案. RQ易于使用,并且非常好地覆盖了简单的用例, 但如果需要更高级的选择, 其他Python 3队列解决方案(例如 芹菜)可以使用.

多处理和. Python中的多线程

如果您的代码是IO绑定的,那么Python中的多处理和多线程都可以为您工作. Python多处理比线程化更容易实现,但它的内存开销更高. 如果您的代码是CPU限制的, 多处理很可能是更好的选择—特别是如果目标机器有多个内核或cpu. web应用程序, 当您需要跨多台机器扩展工作时, RQ会更适合你.


 

更新

改进Python 3中的并发性.2 + 并发.期货

Python 3以来的新功能.原文中没有提到的第二个问题是 并发.期货 包. 这个包提供了另一种在Python中使用并行性和并发性的方法.

在原文中, 我提到过,Python多处理模块比线程模块更容易放入现有代码中. 这是因为Python 3的线程模块需要子类化 线程 类,还创建了 队列 用于监视工作的线程.

使用一个 并发.期货.线程池Executor 使得Python线程示例代码几乎与多处理模块相同.

导入日志
进口操作系统
从并发.导入线程池Executor
从functools导入partial
从时间导入时间

从下载导入setup_下载_dir, get_links, 下载_link

日志记录.basicConfig(=日志级别.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

日志记录.getlog (__name__)


def main ():
    Client_id = OS.采用“IMGUR_CLIENT_ID”)
    如果不是client_id:
        无法找到IMGUR_CLIENT_ID环境变量!")
    Download_dir = setup_下载_dir()
    链接= get_links(client_id)

    #通过将遗嘱执行人放置在with块中,遗嘱执行人关闭方法
    #将被称为清理线程.
    # 
    #默认情况下,执行器将工人的数量设置为5倍
    # cpu.
    使用线程池Executor()作为执行器:

        创建一个新的部分应用函数来存储目录
        #参数.
        # 
        #这允许下载_link函数,通常需要两个参数
        #参数与map函数一起工作,该函数需要a的函数
        #单参数.
        Fn = partial(下载_link, 下载_dir)

        #使用可迭代链接上的线程并发执行fn. 的
        # timeout适用于整个进程,而不是单个调用,所以请下载
        所有图像必须在30秒内完成.
        遗嘱执行人.Map (fn, links, timeout=30)


如果__name__ == '__main__':
    main ()

现在我们已经用Python下载了所有这些图像 线程池Executor,我们可以使用它们来测试cpu绑定的任务. 我们可以在单线程中创建所有图像的缩略图版本, 单进程脚本,然后测试基于多进程的解决方案.

我们要用 枕头 库来处理图像的大小调整.

这是我们的初始脚本.

导入日志
从路径lib导入路径
从时间导入时间

从PIL导入图像

日志记录.basicConfig(=日志级别.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

日志记录.getlog (__name__)


Def create_thumbnail(大小,路径):
    """
    创建与图像名称相同的图像缩略图
    _thumbnail附加在扩展名之前.  E.g.:

    >>> create_thumbnail((128, 128), '图像.jpg”)

    以图像_thumbnail的名称创建一个新的缩略图.jpg

    :param size:图像的宽度和高度的元组
    :param 路径:镜像文件的路径
    返回:没有
    """
    图像 =图像.打开(路径)
    图像.缩略图(尺寸)
    路径 =路径(路径)
    Name = 路径.茎+ '_thumbnail' +路径.后缀
    Thumbnail_路径 = 路径.with_name(名字)
    图像.保存(thumbnail_路径)


def main ():
    Ts = time()
    for 图像_路径 in Path('图片').iterdir ():
        Create_thumbnail ((128, 128), 图像_路径)
    日志记录.info('占用%s', time() - s)


如果__name__ == '__main__':
    main ()

中的路径进行迭代 图片 文件夹,并为每个路径运行create_thumbnail函数. 这个函数使用枕头打开图像, 创建缩略图, 保存新的, 较小的图像,与原始图像名称相同,但带有 _thumbnail 附加到名称后.

在160张总共3600万张的图片上运行这个脚本需要2个小时.32秒. 看看能不能用a加快速度 Process池Executor.

导入日志
从路径lib导入路径
从时间导入时间
从functools导入partial

从并发.导入Process池Executor

从PIL导入图像

日志记录.basicConfig(=日志级别.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

日志记录.getlog (__name__)


Def create_thumbnail(大小,路径):
    """
    创建与图像名称相同的图像缩略图
    _thumbnail附加在扩展名之前. E.g.:

    >>> create_thumbnail((128, 128), '图像.jpg”)

    以图像_thumbnail的名称创建一个新的缩略图.jpg

    :param size:图像的宽度和高度的元组
    :param 路径:镜像文件的路径
    返回:没有
    """
    路径 =路径(路径)
    Name = 路径.茎+ '_thumbnail' +路径.后缀
    Thumbnail_路径 = 路径.with_name(名字)
    图像 =图像.打开(路径)
    图像.缩略图(尺寸)
    图像.保存(thumbnail_路径)


def main ():
    Ts = time()
    #部分应用create_thumbnail方法,设置大小为128x128
    #并返回单个参数的函数.
    Thumbnail_128 = partial(create_thumbnail, (128, 128))

    #在with块中创建执行器,这样当block被关闭时就会被调用
    #退出.
    使用Process池Executor()作为执行器:
        遗嘱执行人.地图(thumbnail_128路径('图像').iterdir ())
    日志记录.info('占用%s', time() - s)


如果__name__ == '__main__':
    main ()

create_thumbnail 方法与上一个脚本相同. 主要区别在于创建 Process池Executor. 执行程序的 map 方法用于并行创建缩略图. 默认情况下, Process池Executor 为每个CPU创建一个子进程. 在同样的160张图片上运行这个脚本花费了1.05年seconds-2.快2倍!

Async/Await (Python 3.5 +)

在原文章的评论中,请求最多的项目之一是使用Python 3的示例 asyncio 模块. 和其他例子相比, 有一些新的Python语法对大多数人来说可能是新的,也有一些新的概念. 一个不幸的额外复杂性层是由Python的内置 urllib 模块不是异步的. 我们需要使用异步HTTP库来获得asyncio的全部好处. 对此,我们将使用 aiohttp.

让我们直接进入代码,后面会有更详细的解释.

进口asyncio
导入日志
进口操作系统
从时间导入时间

进口aiohttp

从下载导入setup_下载_dir, get_links

日志记录.basicConfig(=日志级别.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
日志记录.getlog (__name__)


Async def async_下载_link(会话,目录,链接):
    """
    我们在其他示例中使用的下载_link方法的Async版本.
    :参数session: aiohttp ClientSession
    :param directory:保存下载文件的目录
    :param link:要下载的链接的url
    返回:
    """
    Download_路径 = directory / OS.路径.basename(链接)
    带会话的异步.获取(链接)作为响应:
        与下载_路径.打开('wb')作为f:
            而真正的:
                # 等待暂停执行,直到从流中读取1024(或更少)字节
                Chunk =等待响应.内容.阅读(1024)
                如果不是chunk:
                    #我们完成了文件的读取,跳出而循环
                    打破
                f.写(块)
    日志记录器.info('已下载%s',链接)


# Main现在是一个协程
Async def ():
    Client_id = OS.采用“IMGUR_CLIENT_ID”)
    如果不是client_id:
        无法找到IMGUR_CLIENT_ID环境变量!")
    Download_dir = setup_下载_dir()
    我们使用会话来利用tcp的keep-alive
    设置3秒的读取和连接超时. 默认为5分钟
    与aiohttp异步.ClientSession(conn_timeout=3, read_timeout=3)作为session:
        Tasks = [(async_下载_link(session, 下载_dir, l)) for lin get_links(client_id)]
        # gather聚合所有任务并在事件循环中调度它们
        等待asyncio.收集(*任务,return_exceptions = True)


如果__name__ == '__main__':
    Ts = time()
    创建asyncio事件循环
    循环= asyncio.get_event_循环 ()
    试一试:
        循环.run_until_complete (main ())
    最后:
        #即使有异常也关闭循环
        循环.close ()
    日志记录器.info('耗时%s秒完成',time() - ts)

这里有很多东西需要解开. 让我们从程序的主要入口点开始. 我们使用asyncio模块做的第一件新事情是获取事件循环. 事件循环处理所有异步代码. 然后循环运行,直到完成并通过 main 函数. 在main的定义中有一个新的语法: 异步def. 你还会注意到 等待与异步.

async/等待语法是在 PEP492. 的 异步def 语法将函数标记为 协同程序. 在内部,协程是基于Python生成器的,但并不是完全相同的东西. 协程返回一个协程对象,类似于生成器返回生成器对象的方式. 有了协程后,就可以使用 等待 表达式. 当协程调用时 等待,则暂停协程的执行,直到可等待对象完成. 此挂起允许在协程挂起“等待”某些结果时完成其他工作. 在一般情况下, 这个结果将是某种I/O,比如数据库请求,或者在我们的例子中是HTTP请求.

下载_link 功能必须有很大的改变. 以前,我们依靠 urllib 为我们做读图的首要工作. 现在,为了使我们的方法能够在异步编程范例中正常工作,我们引入了 循环,每次读取映像的块,并在等待I/O完成时暂停执行. 这允许事件循环遍历下载不同的图像,因为每个图像在下载期间都有新的可用数据.

应该有一种——最好只有一种——显而易见的方法

Python之禅 告诉我们应该有一个显而易见的方法去做某件事, 在Python中有很多方法可以将并发引入我们的程序中. 选择的最佳方法将取决于您的具体用例. 与线程或多处理相比,异步范式更适合于高并发工作负载(如web服务器), 但它要求您的代码(和依赖项)是异步的,以便充分受益.

希望本文中的Python多线程示例(以及更新)将为您指明正确的方向,以便您知道如果需要在程序中引入并发性,应该在Python标准库中查找什么地方.

了解基本知识

  • Python中的线程是什么?

    线程是轻量级进程或任务. 线程是将并发性添加到程序中的一种方法. 如果您的Python应用程序使用多个线程,并且您查看在操作系统上运行的进程, 即使脚本运行多个线程,您也只能看到一个条目.

  • 什么是多线程??

    多线程(有时简称“线程”)是指程序创建多个线程并在它们之间循环执行, 因此,一个长时间运行的任务不会阻碍所有其他任务. 这对于那些可以分解成更小的子任务的任务来说非常有效, 然后每个线程可以被赋予一个线程来完成.

  • Python线程和多处理之间的区别是什么?

    与线程, 并发是使用多个线程实现的, 但是由于吉尔,一次只能运行一个线程. 在多处理, 原始进程被分支成多个绕过吉尔的子进程. 每个子进程将拥有整个程序内存的副本.

  • Python多线程和多处理是如何相关的?

    多线程和多处理都允许Python代码并发运行. 只有多处理才能使代码真正实现并行. 然而, 如果您的代码是io繁重的(如HTTP请求), 那么多线程仍然可能会加快代码的速度.

就这一主题咨询作者或专家.
预约电话
马库斯·麦柯迪

位于 费城,美国宾夕法尼亚州

成员自 2014年12月22日

作者简介

Marcus是一个有才华的程序员,擅长后端开发. 然而,他很适应全栈开发.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

以前在

海军海上系统司令部

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.