第27篇:Python开发进阶:python多线程与多进程编程

devtools/2025/2/3 10:06:51/

第27篇:多线程与多进程编程

目录

  1. 并发编程概述
    • 什么是并发编程
    • 多线程与多进程的区别
  2. 多线程编程
    • 线程的基本概念
    • 创建和管理线程
    • 线程同步与锁
  3. 多进程编程
    • 进程的基本概念
    • 创建和管理进程
    • 进程间通信
  4. 线程与进程的比较
  5. 全局解释器锁(GIL)
    • GIL的影响
    • 绕过GIL的策略
  6. 异步编程简介
    • 异步与并发
    • asyncio模块
  7. 示例代码
  8. 常见问题及解决方法
  9. 总结

并发编程概述

什么是并发编程

并发编程是一种程序设计范式,允许多个任务在同一时间段内交替执行,从而提高程序的执行效率和响应能力。在现代计算机中,并发编程常用于处理多任务、提高资源利用率和优化性能。

多线程与多进程的区别

特性多线程多进程
基本单位线程(Thread)进程(Process)
内存空间共享同一进程的内存空间每个进程拥有独立的内存空间
资源开销较低,线程之间切换开销较小较高,进程之间切换开销较大
数据共享通过共享内存实现,需注意线程同步通过进程间通信(IPC)实现
稳定性一个线程崩溃可能影响整个进程一个进程崩溃不会直接影响其他进程
使用场景IO密集型任务、需要频繁共享数据的场景CPU密集型任务、需要高度隔离的场景

多线程编程

线程的基本概念

线程是程序执行的最小单位,一个进程可以包含多个线程。这些线程共享进程的资源,如内存空间和文件句柄,但每个线程有自己的执行栈和程序计数器。

创建和管理线程

在Python中,可以使用threading模块创建和管理线程。

python">import threading
import timedef worker(name):print(f"线程 {name} 开始工作。")time.sleep(2)print(f"线程 {name} 完成工作。")# 创建线程
thread1 = threading.Thread(target=worker, args=("A",))
thread2 = threading.Thread(target=worker, args=("B",))# 启动线程
thread1.start()
thread2.start()# 等待线程完成
thread1.join()
thread2.join()print("所有线程已完成。")

输出

线程 A 开始工作。
线程 B 开始工作。
线程 A 完成工作。
线程 B 完成工作。
所有线程已完成。

线程同步与锁

由于多个线程共享同一进程的内存空间,可能会导致数据竞争和不一致的问题。为了解决这些问题,需要使用**锁(Lock)**来同步线程。

python">import threadingclass Counter:def __init__(self):self.value = 0self.lock = threading.Lock()def increment(self):with self.lock:temp = self.valuetemp += 1self.value = tempdef worker(counter, num_increments):for _ in range(num_increments):counter.increment()counter = Counter()
threads = []
num_threads = 10
num_increments = 1000# 创建并启动线程
for i in range(num_threads):thread = threading.Thread(target=worker, args=(counter, num_increments))threads.append(thread)thread.start()# 等待所有线程完成
for thread in threads:thread.join()print(f"最终计数值:{counter.value}")  # 应输出: 10000

输出

最终计数值:10000

说明:通过使用锁,确保同一时间只有一个线程可以修改value,避免了数据竞争。


多进程编程

进程的基本概念

进程是程序执行的独立单位,拥有自己的内存空间和系统资源。与线程不同,进程之间相互独立,彼此之间的数据不共享。

创建和管理进程

在Python中,可以使用multiprocessing模块创建和管理进程。

python">import multiprocessing
import timedef worker(name):print(f"进程 {name} 开始工作。")time.sleep(2)print(f"进程 {name} 完成工作。")if __name__ == "__main__":process1 = multiprocessing.Process(target=worker, args=("A",))process2 = multiprocessing.Process(target=worker, args=("B",))process1.start()process2.start()process1.join()process2.join()print("所有进程已完成。")

输出

进程 A 开始工作。
进程 B 开始工作。
进程 A 完成工作。
进程 B 完成工作。
所有进程已完成。

进程间通信

由于进程拥有独立的内存空间,进程间通信(IPC)需要使用特定的机制,如队列(Queue)、管道(Pipe)等。

python">import multiprocessingdef producer(queue):for i in range(5):item = f"Item {i}"queue.put(item)print(f"生产者生产:{item}")def consumer(queue):while True:item = queue.get()if item is None:breakprint(f"消费者消费:{item}")if __name__ == "__main__":q = multiprocessing.Queue()producer_process = multiprocessing.Process(target=producer, args=(q,))consumer_process = multiprocessing.Process(target=consumer, args=(q,))producer_process.start()consumer_process.start()producer_process.join()q.put(None)  # 发送终止信号给消费者consumer_process.join()print("生产与消费完成。")

输出

生产者生产:Item 0
生产者生产:Item 1
生产者生产:Item 2
生产者生产:Item 3
生产者生产:Item 4
消费者消费:Item 0
消费者消费:Item 1
消费者消费:Item 2
消费者消费:Item 3
消费者消费:Item 4
生产与消费完成。

说明:使用队列实现生产者和消费者之间的通信,通过在队列中放置None作为终止信号,通知消费者结束消费。


线程与进程的比较

特性多线程多进程
创建开销低,启动和销毁速度快高,启动和销毁速度慢
内存使用共享内存,内存利用率高独立内存,不共享,内存利用率低
数据共享直接共享,需同步机制需使用IPC机制
稳定性一个线程崩溃可能影响整个进程一个进程崩溃不会影响其他进程
使用场景IO密集型任务、需要频繁共享数据的场景CPU密集型任务、高度隔离的任务

选择建议

  • 多线程适用于IO密集型任务,如网络请求、文件读写等,因为线程之间的切换开销较小,可以有效利用等待时间。
  • 多进程适用于CPU密集型任务,如大规模数据处理、复杂计算等,因为每个进程可以利用多核CPU,提高并行计算能力。

全局解释器锁(GIL)

GIL的影响

在CPython(Python的标准实现)中,存在全局解释器锁(GIL),它保证在任何时刻只有一个线程可以执行Python字节码。这意味着即使在多核CPU上,多线程程序也无法实现真正的并行计算,限制了多线程在CPU密集型任务中的性能提升。

绕过GIL的策略

为了绕过GIL的限制,可以采用以下策略:

  1. 使用多进程:由于每个进程有自己的解释器和GIL,多进程可以充分利用多核CPU,实现真正的并行计算。
  2. 使用C扩展或Cython:通过编写C扩展或使用Cython,可以在C层面释放GIL,实现并行计算。
  3. 使用multiprocessing模块:在Python中,multiprocessing模块提供了方便的多进程支持,适用于需要绕过GIL的场景。

异步编程简介

异步与并发

异步编程是一种并发编程范式,通过事件循环和非阻塞IO操作,实现高效的任务调度和资源利用。与多线程和多进程不同,异步编程通常在单线程内通过协程(coroutine)实现并发。

asyncio模块

Python的asyncio模块提供了用于编写异步代码的框架,支持协程、任务、事件循环等。

python">import asyncioasync def greet(name):print(f"Hello, {name}!")await asyncio.sleep(1)print(f"Goodbye, {name}!")async def main():await asyncio.gather(greet("Alice"),greet("Bob"),greet("Charlie"))if __name__ == "__main__":asyncio.run(main())

输出

Hello, Alice!
Hello, Bob!
Hello, Charlie!
Goodbye, Alice!
Goodbye, Bob!
Goodbye, Charlie!

说明:通过asyncio.gather并行执行多个协程,实现高效的异步任务调度。


示例代码

以下示例展示了如何使用多线程和多进程编写一个简单的网页抓取工具。

项目结构

web_crawler/__init__.pycrawler.py
main.py

web_crawler/crawler.py

python"># web_crawler/crawler.pyimport requests
from threading import Thread
from multiprocessing import Process, Queue
import timeclass ThreadCrawler:def __init__(self, urls):self.urls = urlsdef fetch(self, url):try:response = requests.get(url, timeout=5)print(f"线程抓取 {url} 状态码: {response.status_code}")except requests.RequestException as e:print(f"线程抓取 {url} 失败: {e}")def run(self):threads = []for url in self.urls:thread = Thread(target=self.fetch, args=(url,))threads.append(thread)thread.start()for thread in threads:thread.join()class ProcessCrawler:def __init__(self, urls, queue):self.urls = urlsself.queue = queuedef fetch(self, url):try:response = requests.get(url, timeout=5)result = f"进程抓取 {url} 状态码: {response.status_code}"except requests.RequestException as e:result = f"进程抓取 {url} 失败: {e}"self.queue.put(result)def run(self):processes = []for url in self.urls:process = Process(target=self.fetch, args=(url,))processes.append(process)process.start()for process in processes:process.join()while not self.queue.empty():print(self.queue.get())

main.py

python"># main.pyfrom web_crawler.crawler import ThreadCrawler, ProcessCrawler
from multiprocessing import Queue
import timedef main():urls = ["https://www.python.org","https://www.github.com","https://www.invalid-url.com",  # 无效URL"https://www.stackoverflow.com","https://www.google.com"]print("使用多线程进行网页抓取:")thread_crawler = ThreadCrawler(urls)start_time = time.time()thread_crawler.run()end_time = time.time()print(f"多线程抓取耗时:{end_time - start_time:.2f} 秒\n")print("使用多进程进行网页抓取:")queue = Queue()process_crawler = ProcessCrawler(urls, queue)start_time = time.time()process_crawler.run()end_time = time.time()print(f"多进程抓取耗时:{end_time - start_time:.2f} 秒")if __name__ == "__main__":main()

运行结果

使用多线程进行网页抓取:
线程抓取 https://www.python.org 状态码: 200
线程抓取 https://www.github.com 状态码: 200
线程抓取 https://www.invalid-url.com 失败: HTTPSConnectionPool(host='www.invalid-url.com', port=443): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f8c1c2d2d90>: Failed to establish a new connection: [Errno -2] Name or service not known'))
线程抓取 https://www.stackoverflow.com 状态码: 200
线程抓取 https://www.google.com 状态码: 200
多线程抓取耗时:2.02 秒使用多进程进行网页抓取:
进程抓取 https://www.python.org 状态码: 200
进程抓取 https://www.github.com 状态码: 200
进程抓取 https://www.invalid-url.com 失败: HTTPSConnectionPool(host='www.invalid-url.com', port=443): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f8c1c2d2f40>: Failed to establish a new connection: [Errno -2] Name or service not known'))
进程抓取 https://www.stackoverflow.com 状态码: 200
进程抓取 https://www.google.com 状态码: 200
多进程抓取耗时:2.25 秒

说明:通过多线程和多进程分别抓取多个网页,展示了并发编程在IO密集型任务中的应用。注意,无效的URL会触发异常处理机制,确保程序的健壮性。


常见问题及解决方法

问题1:如何在线程中共享数据?

原因:多个线程需要访问和修改共享数据,可能导致数据竞争和不一致。

解决方法

  1. 使用锁(Lock):在访问共享数据时,使用锁来同步线程,确保同一时间只有一个线程可以修改数据。
  2. 使用线程安全的数据结构:如queue.Queue,它内部实现了线程同步机制,适合多线程环境下的数据交换。
python">import threading
import queuedef producer(q):for i in range(5):q.put(i)print(f"生产者生产:{i}")def consumer(q):while True:item = q.get()if item is None:breakprint(f"消费者消费:{item}")q = queue.Queue()
producer_thread = threading.Thread(target=producer, args=(q,))
consumer_thread = threading.Thread(target=consumer, args=(q,))producer_thread.start()
consumer_thread.start()producer_thread.join()
q.put(None)  # 发送终止信号
consumer_thread.join()

输出

生产者生产:0
生产者生产:1
生产者生产:2
生产者生产:3
生产者生产:4
消费者消费:0
消费者消费:1
消费者消费:2
消费者消费:3
消费者消费:4

问题2:如何避免死锁?

原因:当多个线程或进程相互等待对方释放资源时,会导致程序无法继续执行,即发生死锁。

解决方法

  1. 遵循锁的顺序:确保所有线程以相同的顺序获取锁,避免循环等待。
  2. 使用定时锁:在获取锁时设置超时时间,如果超时则释放已获取的锁,避免长时间等待。
  3. 减少锁的使用范围:尽量缩小锁的粒度,减少锁的持有时间。
python">import threading
import timelock1 = threading.Lock()
lock2 = threading.Lock()def thread_a():with lock1:print("线程A获取lock1")time.sleep(1)with lock2:print("线程A获取lock2")def thread_b():with lock2:print("线程B获取lock2")time.sleep(1)with lock1:print("线程B获取lock1")t1 = threading.Thread(target=thread_a)
t2 = threading.Thread(target=thread_b)t1.start()
t2.start()t1.join()
t2.join()

解决方法示例:通过确保所有线程以相同的顺序获取锁,避免了线程A先获取lock1再获取lock2,而线程B先获取lock2再获取lock1,从而避免了死锁。

问题3:如何在多进程中共享数据?

原因:每个进程拥有独立的内存空间,直接共享数据需要使用进程间通信(IPC)机制。

解决方法

  1. 使用multiprocessing.Queue:适合生产者-消费者模型。
  2. 使用multiprocessing.Pipe:适合点对点通信。
  3. 使用共享内存(Value、Array):适合共享简单数据类型。
python">import multiprocessingdef worker(shared_list, lock):with lock:shared_list.append('data')if __name__ == "__main__":manager = multiprocessing.Manager()shared_list = manager.list()lock = multiprocessing.Lock()processes = []for _ in range(5):p = multiprocessing.Process(target=worker, args=(shared_list, lock))processes.append(p)p.start()for p in processes:p.join()print(f"共享列表内容:{list(shared_list)}")  # 输出: ['data', 'data', 'data', 'data', 'data']

问题4:如何在异步编程中处理异常?

原因:在异步任务中,异常可能不会像同步代码那样立即显现,需要特殊处理。

解决方法

  1. 使用try...except在协程内部捕获异常
  2. 在任务中处理异常,并在事件循环中进行日志记录或其他处理
python">import asyncioasync def faulty_task():try:await asyncio.sleep(1)raise ValueError("这是一个异步异常。")except ValueError as e:print(f"捕获异常:{e}")async def main():await asyncio.gather(faulty_task(),faulty_task())if __name__ == "__main__":asyncio.run(main())

输出

捕获异常:这是一个异步异常。
捕获异常:这是一个异步异常。

总结

在本篇文章中,我们深入探讨了Python中的多线程与多进程编程。通过理解线程和进程的基本概念、学习如何创建和管理线程与进程、掌握线程同步与进程间通信的方法,以及了解全局解释器锁(GIL)的影响,您可以编写更加高效和健壮的并发程序。同时,介绍了异步编程的基本概念和asyncio模块的使用,帮助您在不同的并发模型中做出合理的选择。

学习建议

  1. 多练习并发编程:尝试编写多线程和多进程程序,理解它们在不同场景下的应用和性能差异。
  2. 掌握同步机制:深入学习锁、信号量等同步机制,确保并发程序的正确性和稳定性。
  3. 理解GIL的限制:了解GIL对多线程程序的影响,合理选择并发模型,提升程序性能。
  4. 探索异步编程:学习asyncio模块的高级特性,编写高效的异步任务,提高程序的响应速度。
  5. 阅读并发编程相关书籍和文档:如《Python并发编程实战》,系统性地提升并发编程能力。

如果您有任何问题或需要进一步的帮助,请随时在评论区留言或联系相关技术社区。


http://www.ppmy.cn/devtools/155685.html

相关文章

对比DeepSeek、ChatGPT和Kimi的学术写作撰写引言能力

引言 引言部分引入研究主题&#xff0c;明确研究背景、问题陈述&#xff0c;并提出研究的目的和重要性&#xff0c;最后&#xff0c;概述研究方法和论文结构。 下面我们使用DeepSeek、ChatGPT4以及Kimi辅助引言撰写。 提示词&#xff1a; 你现在是一名[计算机理论专家]&#…

2025.2.1总结

今天又过了一天&#xff0c;过得确实太快了&#xff0c;走了一天的 亲戚。 过年对于小孩子是好事&#xff0c;对于成年人就不一样了。在外混得好的还好说&#xff0c;混得不好的过年真的是心累。这消息也太灵通了&#xff0c;怎么一个个都知道我已经出来上班了。今天不知道是第…

使用 PaddlePaddle 实现逻辑回归:从训练到模型保存与加载

在机器学习中&#xff0c;逻辑回归是一种经典的分类算法&#xff0c;广泛应用于二分类问题。今天&#xff0c;我们将通过一个简单的例子&#xff0c;使用 PaddlePaddle 框架实现逻辑回归模型&#xff0c;并展示如何保存和加载模型&#xff0c;以便进行后续的预测。 1. 简介 逻…

排查定位jar包大文件

解压 JAR 包&#xff1a; mkdir jar_contents unzip your-jar-file.jar -d jar_contents统计各文件大小&#xff1a; du -ah jar_contents | sort -rh | head -n 20这会列出 JAR 包中最大的文件或目录&#xff0c;方便你定位大文件。 方法 2&#xff1a;使用 jar 工具查看文件…

使用eNSP配置GRE VPN实验

实验拓扑 实验需求 1.按照图示配置IP地址 2.在R1和R3上配置默认路由使公网区域互通 3.在R1和R3上配置GRE VPN&#xff0c;使两端私网能够互相访问&#xff0c;Tunne1口IP地址如图 4.在R1和R3上配置RIPv2来传递两端私网路由 实验步骤 GRE VPN配置方法&#xff1a; 发送端&#x…

【Rust】18.2. 可辩驳性:模式是否会无法匹配

喜欢的话别忘了点赞、收藏加关注哦&#xff08;加关注即可阅读全文&#xff09;&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 18.2.1. 模式的两种形式 模式有两种形式&#xff1a; 可辩驳的&#xff08;可失败的&…

新一代搜索引擎,是 ES 的15倍?

Manticore Search介绍 Manticore Search 是一个使用 C 开发的高性能搜索引擎&#xff0c;创建于 2017 年&#xff0c;其前身是 Sphinx Search 。Manticore Search 充分利用了 Sphinx&#xff0c;显着改进了它的功能&#xff0c;修复了数百个错误&#xff0c;几乎完全重写了代码…

C#面向对象(封装)

1.什么是封装? C# 封装 封装 被定义为“把一个或多个项目封闭在一个物理的或者逻辑的包中”。 在面向对象程序设计方法论中&#xff0c;封装是为了防止对实现细节的访问。 抽象和封装是面向对象程序设计的相关特性。 抽象允许相关信息可视化&#xff0c;封装则使开发者实现所…