Python多线程与多进程浅析之三

简介:

基于 I/O 的多线程

多线程的例子中比较多的就是抓取网页,因为抓取网页是典型的 I/O 开销,因此 Python 的多线程终于不显得那么鸡肋了。

我们把上面例子中的计算函数修改为抓取网站的大小。先用最标准的方式,不用线程。

# 标准方式抓取
>>> from time import time
>>> import requests

>>> list_url = ['http://www.qq.com', 'http://chuangyiji.com',
...  'http://taobao.com', 'http://mingrihui.com']

>>> def get_url_size(url):
...     rq = requests.get(url)
...     length = len(rq.content)
...     print(url, length)
...     return length

>>> start = time()

>>> for url in list_url:
...     get_url_size(url)
       
>>> end = time()
>>> print('\ncost time {:f} s'.format(end - start))
http://www.qq.com 246846
http://chuangyiji.com 84537
http://taobao.com 123926
http://mingrihui.com 43480

cost time 11.283091 s

我在里面故意放了两个自己的网站(你没见过的那两个域名就是),一个网站是在国外,一个网站在国内的云主机上,相对访问速度比较慢,因此在执行程序的时候有时候会有明显的等待。对四个网站处理完,差不多要20秒左右或者更多。大家可以看到结果呈现的顺序是和列表中一样的。这是一个单线程的例子。

然后我们修改为上面多线程的模式,程序逻辑几乎一模一样。

# 多线程方式执行网站大小抓取
>>> from time import time
>>> import requests
>>> import threading

>>> list_url = ['http://www.qq.com', 'http://chuangyiji.com',
...  'http://taobao.com', 'http://mingrihui.com']

>>> def get_url_size(url):
...     rq = requests.get(url)
...     length = len(rq.content)
...     print( url, length)
...     return length

>>> start = time()

>>> Threads = []

>>> for url in list_url:
...     thread = threading.Thread(target=get_url_size, args=(url,))
...     thread.start()
...     Threads.append(thread)
    
>>> for thread in Threads:
...     thread.join()
       
>>> end = time()
>>> print('\ncost time {:f} s'.format(end - start))
http://www.qq.com 246836
http://mingrihui.com 43480
http://taobao.com 123926
http://chuangyiji.com 84537

cost time 5.828597 s

可以看到总的来说执行速度快了很多,并且通过显示的网页大小结果,我们会发现,和上面的顺序不一定一样(我自己测试了很多次都不一样),QQ 和淘宝都很快,在国内云上第三,在国外的网站最后,基本都是这个顺序。

毋庸多言,这就是多线程比较适用在非堵塞业务场景的证明。

Python 3.2 开始新增了 concurrent.futures 模块,提供了一种优雅的方式来完成多线程或者多进程的并发实现,我们先"野蛮"一点,使用多进程方式来实现这个功能。

# 多进程方式执行网站大小抓取
>>> from time import time
>>> import requests
>>> import concurrent.futures


>>> list_url = ['http://www.qq.com', 'http://chuangyiji.com', 
... 'http://taobao.com', 'http://mingrihui.com']

>>> def get_url_size(url):
...     rq = requests.get(url)
...     length = len(rq.content)
...     print(url, length)
...     return length

>>> start = time()

>>> pool = concurrent.futures.ProcessPoolExecutor(max_workers=6)

>>> list_result = list(pool.map(get_url_size, list_url))
       
>>> end = time()
>>> print('\ncost time {:f} s'.format(end - start))
http://www.qq.com 246793
http://mingrihui.com 43480
http://chuangyiji.com 84537
http://taobao.com 123918

cost time 8.208078 s

你会发现和前面 CPU 密集运算的例子不同,使用多进程方式并没有提高太多,慢的网站你给它一个单独核心,还是要等待,多线程切换时候,等待的时间我们就已经可以先抓其他的了。

# 多进程方式执行网站大小抓取
# executor 写法
>>> from time import time
>>> import requests
>>> import concurrent.futures

>>> list_url = ['http://www.qq.com', 'http://chuangyiji.com', 
... 'http://taobao.com', 'http://mingrihui.com']

>>> def get_url_size(url):
...     rq = requests.get(url)
...     length = len(rq.content)
...     print(url, length)
...     return length

>>> start = time()

>>> with concurrent.futures.ProcessPoolExecutor(max_workers=6) 
... as executor:
...     # 关键是 submit 方法
...     future = {executor.submit(get_url_size, url): url for url 
...                in list_url}
       
>>> end = time()
>>> print('\ncost time {:f} s'.format(end - start))
http://www.qq.com 246819
http://taobao.com 123918
http://mingrihui.com 43480
http://chuangyiji.com 84537

cost time 4.587851 s

concurrent.futures 模块提供了高级的接口对于异步方式进行执行调用,异步执行可以通过线程池,或者独立的进程池。通过抽象的 Executor 类对于两种调用方式有一致的接口。这样对于我们来说,不管是多线程还是多进程,在代码层面都可以方便的切换。

下面的写法是参照了 Python 3 的官方文档,通过线程池来实现多线程的抓取,所以我把网站 url 增加到8个,通过6个 worker 的线程池来抓取。

# 执行网站大小抓取
# 使用线程池方式

>>> from time import time
>>> import concurrent.futures
>>> import requests

>>> list_url = ['http://www.qq.com', 
...             'http://chuangyiji.com', 
...             'http://taobao.com',
...             'http://www.sohu.com',
...             'http://www.163.com',
...             'http://www.sina.com.cn',
...             'http://www.baidu.com',
...             'http://mingrihui.com']

>>> def get_url_size(url):
...     rq = requests.get(url)
...     length = len(rq.content)
...     return length

>>> start = time()

# 设置了线程池中 worker
>>> with concurrent.futures.ThreadPoolExecutor(max_workers=6) 
... as executor:
...     future_to_url = {executor.submit(get_url_size, url): url for 
...                         url in list_url}
...     for future in concurrent.futures.as_completed(future_to_url):
...         url = future_to_url[future]
...         try:
...             data = future.result()
...         except Exception as exc:
...             print('%r generated an exception: %s' % (url, exc))
...         else:
...             print('%r page is %d bytes' % (url, data))
       
>>> end = time()
>>> print('\ncost time {:f} s'.format(end - start))
'http://www.sohu.com' page is 184564 bytes
'http://www.baidu.com' page is 2381 bytes
'http://www.qq.com' page is 246819 bytes
'http://mingrihui.com' page is 43480 bytes
'http://www.163.com' page is 660241 bytes
'http://www.sina.com.cn' page is 601949 bytes
'http://chuangyiji.com' page is 84537 bytes
'http://taobao.com' page is 123986 bytes

cost time 12.424974 s

小结

Python 的线程在 GIL 的控制之下,线程之间,对整个 Python 解释器,对 Python 提供的 CAPI 的访问,都是互斥的,这可以看作 Python 内核级的互斥机制,这种互斥是我们不能控制的,这样就保护了共享资源。

Python 语言的确是比较讲究简洁以及人类化,有些编程语言的设计为了性能或者独特,使得学习曲线比较陡峭,Python 的解释器有了 GIL 之后会容易实现一些,当然单价就是性能会有影响。(很长一段时间内,Python 没有那么流行就是因为性能的问题,但是随着服务器性能最近几年的直线上升,Python 性能在大多数应用场景下已经不是问题了)

在传统进程、线程之后协程的概念继续发展,异步的操作也使得诸如 Sanic 这样的 Web 框架在性能指标上超过了这几年在整个领域非常厉害的 go 语言,也将 Flask 等"传统"的 Web 框架甩开几乎数量级的差距。

摘自本人与同事所著《Python 机器学习实战》一书

目录
相关文章
|
1月前
|
调度 开发者 Python
深入浅出操作系统:进程与线程的奥秘
在数字世界的底层,操作系统扮演着不可或缺的角色。它如同一位高效的管家,协调和控制着计算机硬件与软件资源。本文将拨开迷雾,深入探索操作系统中两个核心概念——进程与线程。我们将从它们的诞生谈起,逐步剖析它们的本质、区别以及如何影响我们日常使用的应用程序性能。通过简单的比喻,我们将理解这些看似抽象的概念,并学会如何在编程实践中高效利用进程与线程。准备好跟随我一起,揭开操作系统的神秘面纱,让我们的代码运行得更加流畅吧!
|
1月前
|
消息中间件 Unix Linux
【C语言】进程和线程详解
在现代操作系统中,进程和线程是实现并发执行的两种主要方式。理解它们的区别和各自的应用场景对于编写高效的并发程序至关重要。
57 6
|
1月前
|
调度 开发者
深入理解:进程与线程的本质差异
在操作系统和计算机编程领域,进程和线程是两个核心概念。它们在程序执行和资源管理中扮演着至关重要的角色。本文将深入探讨进程与线程的区别,并分析它们在现代软件开发中的应用和重要性。
59 5
|
30天前
|
算法 调度 开发者
深入理解操作系统:进程与线程的管理
在数字世界的复杂编织中,操作系统如同一位精明的指挥家,协调着每一个音符的奏响。本篇文章将带领读者穿越操作系统的幕后,探索进程与线程管理的奥秘。从进程的诞生到线程的舞蹈,我们将一起见证这场微观世界的华丽变奏。通过深入浅出的解释和生动的比喻,本文旨在揭示操作系统如何高效地处理多任务,确保系统的稳定性和效率。让我们一起跟随代码的步伐,走进操作系统的内心世界。
|
1月前
|
调度 开发者
核心概念解析:进程与线程的对比分析
在操作系统和计算机编程领域,进程和线程是两个基本而核心的概念。它们是程序执行和资源管理的基础,但它们之间存在显著的差异。本文将深入探讨进程与线程的区别,并分析它们在现代软件开发中的应用和重要性。
56 4
|
2月前
|
数据采集 存储 数据处理
Python中的多线程编程及其在数据处理中的应用
本文深入探讨了Python中多线程编程的概念、原理和实现方法,并详细介绍了其在数据处理领域的应用。通过对比单线程与多线程的性能差异,展示了多线程编程在提升程序运行效率方面的显著优势。文章还提供了实际案例,帮助读者更好地理解和掌握多线程编程技术。
|
2月前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
2月前
|
监控 JavaScript 前端开发
python中的线程和进程(一文带你了解)
欢迎来到瑞雨溪的博客,这里是一位热爱JavaScript和Vue的大一学生分享技术心得的地方。如果你从我的文章中有所收获,欢迎关注我,我将持续更新更多优质内容,你的支持是我前进的动力!🎉🎉🎉
28 0
|
2月前
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
69 0
|
11天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
34 1