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 机器学习实战》一书

目录
相关文章
|
7天前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
1天前
|
调度 Python
Python多线程、多进程与协程面试题解析
【4月更文挑战第14天】Python并发编程涉及多线程、多进程和协程。面试中,对这些概念的理解和应用是评估候选人的重要标准。本文介绍了它们的基础知识、常见问题和应对策略。多线程在同一进程中并发执行,多进程通过进程间通信实现并发,协程则使用`asyncio`进行轻量级线程控制。面试常遇到的问题包括并发并行混淆、GIL影响多线程性能、进程间通信不当和协程异步IO理解不清。要掌握并发模型,需明确其适用场景,理解GIL、进程间通信和协程调度机制。
12 0
|
9天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信
|
20天前
|
存储 缓存 NoSQL
Redis单线程已经很快了6.0引入多线程
Redis单线程已经很快了6.0引入多线程
31 3
|
23天前
|
消息中间件 安全 Linux
线程同步与IPC:单进程多线程环境下的选择与权衡
线程同步与IPC:单进程多线程环境下的选择与权衡
57 0
|
1月前
|
Java 调度 C#
C#学习系列相关之多线程(一)----常用多线程方法总结
C#学习系列相关之多线程(一)----常用多线程方法总结
|
1月前
|
安全 编译器 C#
C#学习相关系列之多线程---lock线程锁的用法
C#学习相关系列之多线程---lock线程锁的用法
|
1月前
|
Java C#
C#学习系列相关之多线程(五)----线程池ThreadPool用法
C#学习系列相关之多线程(五)----线程池ThreadPool用法
|
1月前
|
存储 安全 Java
深入理解 Java 多线程、Lambda 表达式及线程安全最佳实践
线程使程序能够通过同时执行多个任务而更有效地运行。 线程可用于在不中断主程序的情况下在后台执行复杂的任务。 创建线程 有两种创建线程的方式。 扩展Thread类 可以通过扩展Thread类并覆盖其run()方法来创建线程:
102 1
深入理解 Java 多线程、Lambda 表达式及线程安全最佳实践
|
1月前
|
缓存 安全 Java
保障线程安全性:构建可靠的多线程应用
保障线程安全性:构建可靠的多线程应用