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

目录
相关文章
|
6天前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
10天前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
7天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
18 1
|
13天前
深入理解操作系统:进程与线程的管理
【10月更文挑战第30天】操作系统是计算机系统的核心,它负责管理计算机硬件资源,为应用程序提供基础服务。本文将深入探讨操作系统中进程和线程的概念、区别以及它们在资源管理中的作用。通过本文的学习,读者将能够更好地理解操作系统的工作原理,并掌握进程和线程的管理技巧。
28 2
|
14天前
|
调度 Python
深入浅出操作系统:进程与线程的奥秘
【10月更文挑战第28天】在数字世界的幕后,操作系统悄无声息地扮演着关键角色。本文将拨开迷雾,深入探讨操作系统中的两个基本概念——进程和线程。我们将通过生动的比喻和直观的解释,揭示它们之间的差异与联系,并展示如何在实际应用中灵活运用这些知识。准备好了吗?让我们开始这段揭秘之旅!
|
18天前
|
Java Unix 调度
python多线程!
本文介绍了线程的基本概念、多线程技术、线程的创建与管理、线程间的通信与同步机制,以及线程池和队列模块的使用。文章详细讲解了如何使用 `_thread` 和 `threading` 模块创建和管理线程,介绍了线程锁 `Lock` 的作用和使用方法,解决了多线程环境下的数据共享问题。此外,还介绍了 `Timer` 定时器和 `ThreadPoolExecutor` 线程池的使用,最后通过一个具体的案例展示了如何使用多线程爬取电影票房数据。文章还对比了进程和线程的优缺点,并讨论了计算密集型和IO密集型任务的适用场景。
37 4
|
18天前
|
调度 iOS开发 MacOS
python多进程一文够了!!!
本文介绍了高效编程中的多任务原理及其在Python中的实现。主要内容包括多任务的概念、单核和多核CPU的多任务实现、并发与并行的区别、多任务的实现方式(多进程、多线程、协程等)。详细讲解了进程的概念、使用方法、全局变量在多个子进程中的共享问题、启动大量子进程的方法、进程间通信(队列、字典、列表共享)、生产者消费者模型的实现,以及一个实际案例——抓取斗图网站的图片。通过这些内容,读者可以深入理解多任务编程的原理和实践技巧。
41 1
|
19天前
|
Linux 调度
探索操作系统核心:进程与线程管理
【10月更文挑战第24天】在数字世界的心脏,操作系统扮演着至关重要的角色。它不仅是计算机硬件与软件之间的桥梁,更是管理和调度资源的大管家。本文将深入探讨操作系统的两大基石——进程与线程,揭示它们如何协同工作以确保系统运行得井井有条。通过深入浅出的解释和直观的代码示例,我们将一起解锁操作系统的管理奥秘,理解其对计算任务高效执行的影响。
|
6月前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
6月前
|
数据采集 数据库 C++
python并发编程:并发编程中是选择多线程呢?还是多进程呢?还是多协程呢?
python并发编程:并发编程中是选择多线程呢?还是多进程呢?还是多协程呢?
64 0