本文介绍: Python并发编程有多种实现方式,包括多线程、多进程协程等。其中,多线程通常适用于 I/O 密集型的任务,但由于 GIL 的存在,不能真正发挥出多核处理器性能;而多进程可以真正发挥出多核处理器性能,但进程之间通信数据共享比较麻烦,每个进程的启动销毁也会涉及到一定的开销。协程则是一种轻量级并发处理方式,适用于 I/O 密集型任务和部分计算密集型任务,可以通过 async/await 关键字asyncio 库来实现

听说是鸡肋

一直以来,关于Python多线程和多进程是否是鸡肋的争议一直存在,今晚抽空谈谈我的看法,以下是我的观点:

对于多线程

Python 的多线程库 threading 在某些情况下确实是鸡肋的,这是因为 Python全局解释器锁(Global Interpreter Lock, GIL)导致了多线程的并发性能不能真正发挥出来。简单来说,这意味着在任何给定时刻只有一个线程能够真正地运行 Python 代码,这就限制了多线程的性能

然而,对于一些特定类型的任务,比如 I/O 密集型的任务,多线程还是可以带来性能提升的。这是因为 I/O 操作通常会导致线程阻塞,让其他线程得以运行。此外,在 Python3 中,对于一些特殊情况,比如使用 asyncio 库,也可以通过协程实现并发执行,从而规避 GIL 的限制

对于多进程:

Python 的多进程库 multiprocessing可以真正发挥出多核处理器的性能的,因为每个进程都有自己解释器和 GIL。这意味着每个进程可以独立运行 Python 代码,从而实现真正的并行处理

当然,多进程也有一些缺点,比如进程之间的通信和数据共享比较麻烦。此外,每个进程的启动销毁都会涉及到一定的开销,因此如果任务很小,多进程可能反而会带来性能下降。

多线程和多进程怎么选

对于不同类型的任务,多线程和多进程都有它们的优缺点需要根据具体情况进行选择。如果你要处理的任务是 CPU 密集型的,那么多进程可能更好的选择;如果是 I/O 密集型的,那么多线程可能更合适。

实战验证

  1. 下面我写一个简单代码示例用来说明 Python 多线程在 CPU 密集型任务中的性能问题
import threading

counter = 0

def worker():
    global counter
    for i in range(10000000):
        counter += 1

threads = []
for i in range(4):
    t = threading.Thread(target=worker)
    threads.append(t)

for t in threads:
    t.start()

for t in threads:
    t.join()

print(counter)
复制代码

这个代码示例定义一个全局变量 counter,然后创建了 4 个线程,每个线程都会执行一个简单循环,将 counter 的值加 1。最后输出 counter 的值。

在单线程模式下,循环完成counter 的值应该是 40000000,但是在多线程模式下,由于 GIL 的限制多个线程并不能真正并行执行代码,导致 counter 的最终值小于 40000000。例如,在我的机器上运行这个代码示例,最终的输出结果可能是 36092076,远小于预期的值。

这个示例表明,在一些 CPU 密集型的任务中,Python 多线程的性能受到 GIL 的限制,不能真正地发挥出多核处理器的优势。

  1. 我再写一个简单的代码示例,用来说明在 I/O 密集型任务中,多线程可以带来性能提升的情况:
import threading
import requests

urls = [    "https://www.google.com",    "https://www.baidu.com",    "https://www.github.com",    "https://www.python.org"]

def worker(url):
    res = requests.get(url)
    print(f"{url} : {len(res.content)} bytes")

threads = []
for url in urls:
    t = threading.Thread(target=workerargs=(url,))
    threads.append(t)

for t in threads:
    t.start()

for t in threads:
    t.join()
复制代码

这个代码示例创建了 4 个线程,每个线程负责访问一个 URL 并打印出该 URL 返回内容长度。由于访问 URL 的操作是 I/O 密集型的,因此线程在等待服务器响应时会阻塞,让其他线程有机会执行

在我的机器上运行这个代码示例可以看到 4 个线程几乎同时执行,并在几乎相同时间完成了任务,证明了多线程在 I/O 密集型任务中的性能优势。

对于 Python3 中的 asyncio 库,它提供了基于协程的并发执行模型可以在一定程度上规避 GIL 的限制。下面写了一个简单使用 asyncio 库的代码示例

import asyncio
import aiohttp

urls = [
    "https://www.google.com",
    "https://www.baidu.com",
    "https://www.github.com",
    "https://www.python.org"
]

async def worker(url):
    async with aiohttp.ClientSession() asession:
        async with session.get(url) aresponse:
            content = await response.read()
            print(f"{url} : {len(content)} bytes")

async def main():
    tasks = []
    for url in urls:
        task = asyncio.create_task(worker(url))
        tasks.append(task)
    await asyncio.gather(*tasks)

asyncio.run(main())
复制代码

这个代码示例使用 asyncio 库来实现异步访问多个 URL,其中 worker 函数是一个异步函数,使用 aiohttp发送异步 HTTP 请求。在主函数中使用 asyncio.create_task 创建多个协程任务,并使用 asyncio.gather 函数等待所有协程任务完成

在我的机器上运行这个代码示例,可以看到几乎同时访问 4 个 URL,并在几乎相同时间完成了任务,证明了 asyncio 库在 I/O 密集型任务中的性能优势。

  1. 我们继续看多进程,下面我写了一个简单的代码示例,用来说明 multiprocessing是否可以真正发挥出多核处理器的性能:
import multiprocessing

def worker(start, end):
    for i in range(start, end):
        print(i * i)

if __name__ == '__main__':
    processes = []
    num_processes = 4
    num_tasks = 20

    for i in range(num_processes):
        start = i * num_tasks // num_processes
        end = (i + 1) * num_tasks // num_processes
        p = multiprocessing.Process(target=worker, args=(start, end))
        processes.append(p)

    for p in processes:
        p.start()

    for p in processes:
        p.join()
复制代码

这个代码示例创建了 4 个进程,每个进程负责计算一段整数平方打印结果。由于每个进程有自己解释器和 GIL,因此每个进程可以独立地运行 Python 代码,从而实现真正的并行处理。

在我的机器上运行这个代码示例,可以看到 4 个进程几乎同时执行,并在几乎相同时间完成了任务,证明了 multiprocessing 库可以真正发挥出多核处理器的性能。

  1. 之前提到,多进程在处理小任务时可能会带来性能下降,下面我写了一个简单的代码示例,说明以下这种情况:
import multiprocessing

def worker(num):
    result = num * num
    print(result)

if __name__ == '__main__':
    processes = []
    num_processes = 4

    for i in range(num_processes):
        p = multiprocessing.Process(target=worker, args=(i,))
        processes.append(p)

    for p in processes:
        p.start()

    for p in processes:
        p.join()
复制代码

这个代码示例创建了 4 个进程,每个进程负责计算一个整数平方打印结果。由于任务非常小,每个进程的计算时间非常短,因此进程的启动销毁所涉及的开销可能会成为性能的瓶颈。

在我的机器上运行这个代码示例,可以看到进程的启动销毁所涉及的开销导致整个程序的运行时间远远超过了单进程的运行时间。这说明在处理小任务时,多进程可能会带来性能下降,因此需要根据实际情况选择合适的并发处理方式

最后的总结

Python 的并发编程有多种实现方式,包括多线程、多进程和协程等。其中,多线程通常适用于 I/O 密集型的任务,但由于 GIL 的存在,不能真正发挥出多核处理器的性能;而多进程则可以真正发挥出多核处理器的性能,但进程之间的通信和数据共享比较麻烦,每个进程的启动和销毁也会涉及到一定的开销。协程则是一种轻量级的并发处理方式,适用于 I/O 密集型任务和部分计算密集型任务,可以通过 async/await 关键字和 asyncio 库来实现

在实际编程中,需要根据任务类型数据量、机器配置因素来选择合适的并发处理方式。对于小型任务,多进程可能会带来性能下降;对于计算密集型任务,可以考虑使用多进程或者协程;对于 I/O 密集型任务,可以使用多线程、多进程或者协程等方式。同时,还需要注意并发处理带来的数据竞争死锁、线程安全问题,以保证程序正确性和性能。

 

原文地址:https://blog.csdn.net/Python966/article/details/129953417

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_40640.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注