乐趣区

关于python:听说Python有鸡肋一起聊聊

据说是鸡肋

始终以来,对于 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=worker, args=(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() as session:
        async with session.get(url) as response:
            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 密集型工作,能够应用多线程、多过程或者协程等形式。同时,还须要留神并发解决带来的数据竞争、死锁、线程平安等问题,以保障程序的正确性和性能。

本文转载于 WX 公众号:不背锅运维(喜爱的盆友关注咱们):https://mp.weixin.qq.com/s/XMKKUXZxZwVd_PKevkdxaw

退出移动版