深入解析Python并发编程的多线程和异步编程
本文分享自华为云社区《Python并发编程探秘:多线程与异步编程的深入解析》,作者:柠檬味拥抱。
在Python编程中,多线程是一种常用的并发编程方式,它可以有效地提高程序的执行效率,特别是在处理I/O密集型任务时。Python提供了threading
模块,使得多线程编程变得相对简单。本文将深入探讨threading
模块的基础知识,并通过实例演示多线程的应用。
1. 多线程基础概念
在开始之前,让我们先了解一些多线程编程的基本概念:
- 线程(Thread):是操作系统能够进行运算调度的最小单位,通常在一个进程内部。
- 多线程(Multithreading):是指在同一程序中同时运行多个线程。
- GIL(Global Interpreter Lock):Python解释器的全局解释器锁,限制同一时刻只能有一个线程执行Python字节码,因此在CPU密集型任务中,多线程并不能充分利用多核处理器。
2. threading模块基础
threading
模块提供了创建和管理线程的工具。以下是一些常用的threading
模块中的类和函数:
Thread
类:用于创建线程的类,通过继承Thread
类并实现run
方法来定义线程的执行逻辑。start()
方法:启动线程。join()
方法:等待线程执行结束。active_count()
函数:获取当前活动线程的数量。
3. 代码实战:多线程下载图片
下面通过一个实例来演示多线程的应用,我们将使用多线程来下载一系列图片。
- import threading
- import requests
- from queue import Queue
- class ImageDownloader:
- def __init__(self, urls):
- self.urls = urls
- self.queue = Queue()
- def download_image(self, url):
- response = requests.get(url)
- if response.status_code == 200:
- filename = url.split("/")[-1]
- with open(filename, "wb") as f:
- f.write(response.content)
- print(f"Downloaded: {filename}")
- def worker(self):
- while True:
- url = self.queue.get()
- if url is None:
- break
- self.download_image(url)
- self.queue.task_done()
- def start_threads(self, num_threads=5):
- threads = []
- for _ in range(num_threads):
- thread = threading.Thread(target=self.worker)
- thread.start()
- threads.append(thread)
- for url in self.urls:
- self.queue.put(url)
- self.queue.join()
- for _ in range(num_threads):
- self.queue.put(None)
- for thread in threads:
- thread.join()
- if __name__ == "__main__":
- image_urls = ["url1", "url2", "url3", ...] # 替换为实际图片的URL
- downloader = ImageDownloader(image_urls)
- downloader.start_threads()
这个例子中,我们创建了一个ImageDownloader
类,其中包含了一个worker
方法,用于下载图片。通过多线程,我们能够并行地下载多张图片,提高下载效率。
4. 代码解析
download_image
方法:负责下载图片的具体实现。worker
方法:作为线程的执行逻辑,不断从队列中取出待下载的图片URL,并调用download_image
方法。start_threads
方法:启动指定数量的线程,将图片URL放入队列中,等待所有线程执行完毕。
6. 线程安全与锁机制
在多线程编程中,由于多个线程同时访问共享资源,可能引发竞态条件(Race Condition)。为了避免这种情况,可以使用锁机制来确保在某一时刻只有一个线程能够访问共享资源。
threading
模块中提供了Lock
类,通过它可以创建一个锁,使用acquire
方法获取锁,使用release
方法释放锁。下面是一个简单的示例:
- import threading
- counter = 0
- counter_lock = threading.Lock()
- def increment_counter():
- global counter
- for _ in range(1000000):
- with counter_lock:
- counter += 1
- def main():
- thread1 = threading.Thread(target=increment_counter)
- thread2 = threading.Thread(target=increment_counter)
- thread1.start()
- thread2.start()
- thread1.join()
- thread2.join()
- print("Counter:", counter)
- if __name__ == "__main__":
- main()
这个例子中,我们创建了一个全局变量counter
,并使用锁确保在两个线程同时修改counter
时不会发生竞态条件。
7. 多线程的适用场景
多线程适用于处理I/O密集型任务,如网络请求、文件读写等。在这些场景中,线程可以在等待I/O的过程中让出CPU,让其他线程有机会执行,提高程序整体效率。
然而,在处理CPU密集型任务时,由于Python的GIL,多线程并不能充分利用多核处理器,可能导致性能瓶颈。对于CPU密集型任务,考虑使用多进程编程或其他并发模型。
9. 异常处理与多线程
在多线程编程中,异常的处理可能变得更加复杂。由于每个线程都有自己的执行上下文,异常可能在一个线程中引发,但在另一个线程中被捕获。为了有效地处理异常,我们需要在每个线程中使用合适的异常处理机制。
- import threading
- def thread_function():
- try:
- # 一些可能引发异常的操作
- result = 10 / 0
- except ZeroDivisionError as e:
- print(f"Exception in thread: {e}")
- if __name__ == "__main__":
- thread = threading.Thread(target=thread_function)
- thread.start()
- thread.join()
- print("Main thread continues...")
在这个例子中,线程thread_function
中的除法操作可能引发ZeroDivisionError
异常。为了捕获并处理这个异常,我们在线程的代码块中使用了try-except
语句。
10. 多线程的注意事项
在进行多线程编程时,有一些常见的注意事项需要特别关注:
- 线程安全性:确保多个线程同时访问共享资源时不会引发数据竞争和不一致性。
- 死锁:当多个线程相互等待对方释放锁时可能发生死锁,需要谨慎设计和使用锁。
- GIL限制:Python的全局解释器锁可能限制多线程在CPU密集型任务中的性能提升。
- 异常处理:需要在每个线程中适当处理异常,以防止异常在一个线程中引发但在其他线程中未被捕获。
11. 多线程的性能优化
在一些情况下,我们可以通过一些技巧来优化多线程程序的性能:
- 线程池:使用
concurrent.futures
模块中的ThreadPoolExecutor
来创建线程池,提高线程的重用性。 - 队列:使用队列来协调多个线程之间的工作,实现生产者-消费者模型。
- 避免GIL限制:对于CPU密集型任务,考虑使用多进程、
asyncio
等其他并发模型。
13. 面向对象的多线程设计
在实际应用中,我们通常会面对更复杂的问题,需要将多线程和面向对象设计结合起来。以下是一个简单的例子,演示如何使用面向对象的方式来设计多线程程序:
- import threading
- import time
- class WorkerThread(threading.Thread):
- def __init__(self, name, delay):
- super().__init__()
- self.name = name
- self.delay = delay
- def run(self):
- print(f"{self.name} started.")
- time.sleep(self.delay)
- print(f"{self.name} completed.")
- if __name__ == "__main__":
- thread1 = WorkerThread("Thread 1", 2)
- thread2 = WorkerThread("Thread 2", 1)
- thread1.start()
- thread2.start()
- thread1.join()
- thread2.join()
- print("Main thread continues...")
在这个例子中,我们创建了一个WorkerThread
类,继承自Thread
类,并重写了run
方法,定义了线程的执行逻辑。每个线程被赋予一个名字和一个延迟时间。
14. 多线程与资源管理器
考虑一个场景,我们需要创建一个资源管理器,负责管理某个资源的分配和释放。这时,我们可以使用多线程来实现资源的异步管理。以下是一个简单的资源管理器的示例:
- import threading
- import time
- class ResourceManager:
- def __init__(self, total_resources):
- self.total_resources = total_resources
- self.available_resources = total_resources
- self.lock = threading.Lock()
- def allocate(self, request):
- with self.lock:
- if self.available_resources >= request:
- print(f"Allocated {request} resources.")
- self.available_resources -= request
- else:
- print("Insufficient resources.")
- def release(self, release):
- with self.lock:
- self.available_resources += release
- print(f"Released {release} resources.")
- class UserThread(threading.Thread):
- def __init__(self, name, resource_manager, request, release):
- super().__init__()
- self.name = name
- self.resource_manager = resource_manager
- self.request = request
- self.release = release
- def run(self):
- print(f"{self.name} started.")
- self.resource_manager.allocate(self.request)
- time.sleep(1) # Simulate some work with allocated resources
- self.resource_manager.release(self.release)
- print(f"{self.name} completed.")
- if __name__ == "__main__":
- manager = ResourceManager(total_resources=5)
- user1 = UserThread("User 1", manager, request=3, release=2)
- user2 = UserThread("User 2", manager, request=2, release=1)
- user1.start()
- user2.start()
- user1.join()
- user2.join()
- print("Main thread continues...")
在这个例子中,ResourceManager
类负责管理资源的分配和释放,而UserThread
类表示一个使用资源的用户线程。通过使用锁,确保资源的安全分配和释放。
16. 多线程的调试与性能分析
在进行多线程编程时,调试和性能分析是不可忽视的重要环节。Python提供了一些工具和技术,帮助我们更好地理解和调试多线程程序。
调试多线程程序
使用print
语句:在适当的位置插入print
语句输出关键信息,帮助跟踪程序执行流程。
日志模块:使用Python的logging
模块记录程序运行时的信息,包括线程的启动、结束和关键操作。
pdb调试器:在代码中插入断点,使用Python的内置调试器pdb
进行交互式调试。
- import pdb
- # 在代码中插入断点
- pdb.set_trace()
性能分析多线程程序
使用timeit
模块:通过在代码中嵌入计时代码,使用timeit
模块来测量特定操作或函数的执行时间。
- import timeit
- def my_function():
- # 要测试的代码
- # 测试函数执行时间
- execution_time = timeit.timeit(my_function, number=1)
- print(f"Execution time: {execution_time} seconds")
使用cProfile
模块:cProfile
是Python的性能分析工具,可以帮助查看函数调用及执行时间。
- import cProfile
- def my_function():
- # 要测试的代码
- # 运行性能分析
- cProfile.run("my_function()")
使用第三方工具:一些第三方工具,如line_profiler
、memory_profiler
等,可以提供更详细的性能分析信息,帮助发现性能瓶颈。
- # 安装line_profiler
- pip install line_profiler
- # 使用line_profiler进行性能分析
- kernprof -l script.py
- python -m line_profiler script.py.lprof
17. 多线程的安全性与风险
尽管多线程编程可以提高程序性能,但同时也带来了一些潜在的安全性问题。以下是一些需要注意的方面:
线程安全性:确保共享资源的访问是线程安全的,可以通过锁机制、原子操作等手段进行控制。
死锁:在使用锁的过程中,小心死锁的产生,即多个线程相互等待对方释放资源,导致程序无法继续执行。
资源泄漏:在多线程编程中,容易出现资源未正确释放的情况,例如线程未正确关闭或锁未正确释放。
GIL限制:在CPU密集型任务中,全局解释器锁(GIL)可能成为性能瓶颈,需谨慎选择多线程或其他并发模型。
18. 探索其他并发模型
虽然多线程是一种常用的并发编程模型,但并不是唯一的选择。Python还提供了其他一些并发模型,包括:
多进程编程:通过
multiprocessing
模块实现,每个进程都有独立的解释器和GIL,适用于CPU密集型任务。异步编程:通过
asyncio
模块实现,基于事件循环和协程,适用于I/O密集型任务,能够提高程序的并发性。并行计算:使用
concurrent.futures
模块中的ProcessPoolExecutor
和ThreadPoolExecutor
,将任务并行执行。
19. 持续学习与实践
多线程编程是一个广阔而复杂的领域,本文只是为你提供了一个入门的指南。持续学习和实践是深入掌握多线程编程的关键。
建议阅读Python官方文档和相关书籍,深入了解threading
模块的各种特性和用法。参与开源项目、阅读其他人的源代码,也是提高技能的好方法。
21. 多线程的异步化与协程
在现代编程中,异步编程和协程成为处理高并发场景的重要工具。Python提供了asyncio
模块,通过协程实现异步编程。相比于传统多线程,异步编程可以更高效地处理大量I/O密集型任务,而无需创建大量线程。
异步编程基础
异步编程通过使用async
和await
关键字来定义协程。协程是一种轻量级的线程,可以在运行时暂停和继续执行。
- import asyncio
- async def my_coroutine():
- print("Start coroutine")
- await asyncio.sleep(1)
- print("Coroutine completed")
- async def main():
- await asyncio.gather(my_coroutine(), my_coroutine())
- if __name__ == "__main__":
- asyncio.run(main())
在上述例子中,my_coroutine
是一个协程,使用asyncio.sleep
模拟异步操作。通过asyncio.gather
同时运行多个协程。
异步与多线程的比较
性能: 异步编程相较于多线程,可以更高效地处理大量的I/O密集型任务,因为异步任务在等待I/O时能够让出控制权,不阻塞其他任务的执行。
复杂性: 异步编程相对于多线程来说,编写和理解的难度可能较大,需要熟悉协程的概念和异步编程的模型。
示例:异步下载图片
以下是一个使用异步编程实现图片下载的简单示例:
- import asyncio
- import aiohttp
- async def download_image(session, url):
- async with session.get(url) as response:
- if response.status == 200:
- filename = url.split("/")[-1]
- with open(filename, "wb") as f:
- f.write(await response.read())
- print(f"Downloaded: {filename}")
- async def main():
- image_urls = ["url1", "url2", "url3", ...] # 替换为实际图片的URL
- async with aiohttp.ClientSession() as session:
- tasks = [download_image(session, url) for url in image_urls]
- await asyncio.gather(*tasks)
- if __name__ == "__main__":
- asyncio.run(main())
在这个例子中,通过aiohttp
库创建异步HTTP请求,asyncio.gather
并发执行多个协程。
22. 异步编程的异常处理
在异步编程中,异常的处理方式也有所不同。在协程中,我们通常使用try-except
块或者asyncio.ensure_future
等方式来处理异常。
- import asyncio
- async def my_coroutine():
- try:
- # 异步操作
- await asyncio.sleep(1)
- raise ValueError("An error occurred")
- except ValueError as e:
- print(f"Caught an exception: {e}")
- async def main():
- task = asyncio.ensure_future(my_coroutine())
- await asyncio.gather(task)
- if __name__ == "__main__":
- asyncio.run(main())
在这个例子中,asyncio.ensure_future
将协程包装成一个Task
对象,通过await asyncio.gather
等待任务执行完毕,捕获异常。
23. 异步编程的优势与注意事项
优势
高并发性: 异步编程适用于大量I/O密集型任务,能够更高效地处理并发请求,提高系统的吞吐量。
资源效率: 相较于多线程,异步编程通常更节省资源,因为协程是轻量级的,可以在一个线程中运行多个协程。
注意事项
- 阻塞操作: 异步编程中,阻塞操作会影响整个事件循环,应尽量避免使用阻塞调用。
- 异常处理: 异步编程的异常处理可能更加复杂,需要仔细处理协程中的异常情况。
- 适用场景: 异步编程更适用于I/O密集型任务,而不是CPU密集型任务。
24. 探索更多异步编程工具和库
除了asyncio
和aiohttp
之外,还有一些其他强大的异步编程工具和库:
asyncpg
: 异步PostgreSQL数据库驱动。aiofiles
: 异步文件操作库。aiohttp
: 异步HTTP客户端和服务器框架。aiomysql
: 异步MySQL数据库驱动。uvloop
: 用于替代标准事件循环的高性能事件循环。
25. 持续学习与实践
异步编程是一个广泛且深入的主题,本文只是为你提供了一个简要的介绍。建议深入学习asyncio
模块的文档,理解事件循环、协程、异步操作等概念。
同时,通过实际项目的实践,你将更好地理解和掌握异步编程的技术和最佳实践。
总结
本文深入探讨了Python中的多线程编程和异步编程,涵盖了多线程模块(threading
)的基础知识、代码实战,以及异步编程模块(asyncio
)的基本概念和使用。我们从多线程的基础,如Thread
类、锁机制、线程安全等开始,逐步展示了多线程在实际应用中的应用场景和注意事项。通过一个实例展示了多线程下载图片的过程,强调了线程安全和异常处理的重要性。
随后,本文引入了异步编程的概念,通过协程、async
和await
关键字,以及asyncio
模块的使用,向读者展示了异步编程的基础。通过一个异步下载图片的实例,强调了异步编程在处理I/O密集型任务中的高效性。
文章还对异步编程的异常处理、优势与注意事项进行了详细讨论,同时介绍了一些常用的异步编程工具和库。最后,鼓励读者通过不断学习、实践,深化对多线程和异步编程的理解,提高在并发编程方面的能力。
无论是多线程编程还是异步编程,都是提高程序并发性、性能和响应性的关键技术。通过深入理解这些概念,读者可以更好地应对现代编程中复杂的并发需求,提升自己的编程水平。祝愿读者在多线程和异步编程的学习过程中取得丰硕的成果!
深入解析Python并发编程的多线程和异步编程的更多相关文章
- 初步谈谈 C# 多线程、异步编程与并发服务器
多线程与异步编程可以达到避免调用线程异步阻塞作用,但是两者还是有点不同. 多线程与异步编程的异同: 1.线程是cpu 调度资源和分配的基本单位,本质上是进程中的一段并发执行的代码. 2.线程编程的思维 ...
- 重新想象 Windows 8 Store Apps (44) - 多线程之异步编程: 经典和最新的异步编程模型, IAsyncInfo 与 Task 相互转换
[源码下载] 重新想象 Windows 8 Store Apps (44) - 多线程之异步编程: 经典和最新的异步编程模型, IAsyncInfo 与 Task 相互转换 作者:webabcd 介绍 ...
- 重新想象 Windows 8 Store Apps (45) - 多线程之异步编程: IAsyncAction, IAsyncOperation, IAsyncActionWithProgress, IAsyncOperationWithProgress
[源码下载] 重新想象 Windows 8 Store Apps (45) - 多线程之异步编程: IAsyncAction, IAsyncOperation, IAsyncActionWithPro ...
- 【读书笔记】C#高级编程 第十三章 异步编程
(一)异步编程的重要性 使用异步编程,方法调用是在后台运行(通常在线程或任务的帮助下),并不会阻塞调用线程.有3中不同的异步编程模式:异步模式.基于事件的异步模式和新增加的基于任务的异步模式(TAP, ...
- Python并发复习1 - 多线程
一.基本概念 程序: 指令集,静态, 进程: 当程序运行时,会创建进程,是操作系统资源分配的基本单位 线程: 进程的基本执行单元,每个进程至少包含一个线程,是任务调度和执行的基本单位 > 进程和 ...
- 利用python yielding创建协程将异步编程同步化
转自:http://www.jackyshen.com/2015/05/21/async-operations-in-form-of-sync-programming-with-python-yiel ...
- 用 Python 3 的 async / await 做异步编程
前年我曾写过一篇<初探 Python 3 的异步 IO 编程>,当时只是初步接触了一下 yield from 语法和 asyncio 标准库.前些日子我在 V2EX 看到一篇<为什么 ...
- Python网络编程(4)——异步编程select & epoll
在SocketServer模块的学习中,我们了解了多线程和多进程简单Server的实现,使用多线程.多进程技术的服务端为每一个新的client连接创建一个新的进/线程,当client数量较多时,这种技 ...
- Python Twisted系列教程2:异步编程初探与reactor模式
作者:dave@http://krondo.com/slow-poetry-and-the-apocalypse/ 译者:杨晓伟(采用意译) 这个系列是从这里开始的,欢迎你再次来到这里来.现在我们可 ...
- C# - 多线程 之 异步编程
异步编程 同步编程,请求响应模型,同步化.顺序化.事务化. 异步编程,事件驱动模型,以 Fire and Forget 方式实现. 异步编程模式 -§- 异步编程模型 (APM) 模式: IAsyn ...
随机推荐
- 【转帖】一道面试题:JVM老年代空间担保机制
面试问题 昨天面试的时候,面试官问的问题: 什么是老年代空间担保机制?担保的过程是什么? 老年代空间担保机制是谁给谁担保? 为什么要有老年代空间担保机制?或者说空间担保机制的目的是什么? 如果没有老年 ...
- [转帖]strace 命令详解
目录 1.strace是什么? 2.strace能做什么? 3.strace怎么用? 4.strace问题定位案例 4.1.定位进程异常退出 4.2.定位共享内存异常 4.3. 性能分析 5.总结 1 ...
- [转帖]怎样设计异步系统: Linux Native AIO vs io_uring
https://zhuanlan.zhihu.com/p/149836046 Linux native aio一方面有其实用价值, 基本满足了特别业务比如大型数据库系统对异步io的需求, 另一方面却被 ...
- [转帖]HotSpot 虚拟机对象探秘
https://www.cnblogs.com/xiaojiesir/p/15593092.html 对象的创建 一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和 ...
- true=='true'这个等式成立吗?
在localStorage存入里面的数据是字符串,如果你存入了一个值是Boolean类型的, 那你你取出来就是一个字符串 'true' 或者 'false' 假设取出来的值是 'true' 在你进行i ...
- ILRuntime性能测试
我们公司有一个Unity原生开发语言C#写的项目,目前已经在安卓测试过多次,上架IOS在考虑热更,所以对ILRuntim进行性能测试,在测试过程中已经按照官方文档进行了CLR绑定和生成Release的 ...
- strobe
总是喜欢一个人出神,置身的场景经常是小时有趣的明晃晃的下午.也不知道为什么印象中有趣的下午的阳光总是让人睁不开眼,我也曾试图给大脑传递过"能不能将那晃眼的阳光删去",但再次置身仍是 ...
- Oracle查询存在外键约束但未创建对应索引的情况
1.Oracle提供的脚本 2.网络搜索到的脚本 3.改为可以指定用户的脚本 4.测试脚本使用 1.Oracle提供的脚本 如果要求管控严格,可以考虑使用Oracle官方提供的脚本. Script t ...
- 多个Nginx进程运行导致配置加载失效问题
多个Nginx进程运行导致配置加载失效问题 问题描述 在用nginx进行接口代理时,修改配置文件后,重新加载nginx,却发现无论怎么修改配置文件,都无法生效,接口一直无法代理成功.查看了之前做的接口 ...
- NC16886 [NOI2001]炮兵阵地
题目链接 题目 题目描述 司令部的将军们打算在N*M的网格地图上部署他们的炮兵部队.一个N*M的地图由N行M列组成,地图的每一格可能是山地(用"H" 表示),也可能是平原(用&qu ...