python的一些并发执行案例(多线程、多进程、协程、子进程)


0 前言

Python作为一种广泛应用的编程语言,虽然因其GIL(全局解释器锁)限制,导致在多线程并发执行中存在一定局限性,但它依然提供了多种并发执行方式。通过合理地运用多线程、多进程、协程、以及子进程,开发者可以在不同场景下大幅提升程序性能。

本篇文章将围绕Python的几种主要并发执行模式进行探讨,结合实际案例展示如何在不同任务场景中选择并实现这些技术。我们将重点介绍多线程适用于IO密集型任务的场景,多进程在CPU密集型任务中的优势,协程在高并发异步任务中的强大能力,以及子进程在独立进程间通信中的独特价值。

1 多线程

在这个例子中,我们将通过多线程同时从多个URL下载网页内容。这是一个典型的IO密集型任务,因为下载网页涉及网络IO操作(等待服务器响应、数据传输等),而不是计算。

多线程处理IO密集型任务的示例代码:

import threading
import time

# 定义一个函数,用于从指定的URL下载网页内容
def download_url(url):
    time.sleep(2)  # 模拟下载的延迟

# 定义要下载的URL列表
urls = [
    "https://www.python.org",
    "https://www.github.com",
    "https://www.stackoverflow.com",
    "https://www.reddit.com"
]

# 使用多线程来下载多个URL
def download_with_threads(urls):
    threads = []

    # 为每个URL创建一个线程
    for url in urls:
        thread = threading.Thread(target=download_url, args=(url,))
        threads.append(thread)
        thread.start()  # 启动线程

    # 等待所有线程完成
    for thread in threads:
        thread.join()

# 测试下载性能
start_time = time.time()
download_with_threads(urls)
end_time = time.time()

print(f"Total time taken: {end_time - start_time} seconds")

# 输出内容为:
"Total time taken: 2.007413148880005 seconds"

单线程处理IO密集型任务的示例代码:

import time

# 定义一个函数,用于从指定的URL下载网页内容
def download_url(url):
    time.sleep(2)  # 模拟下载的延迟

# 定义要下载的URL列表
urls = [
    "https://www.python.org",
    "https://www.github.com",
    "https://www.stackoverflow.com",
    "https://www.reddit.com"
]

# 使用多线程来下载多个URL
def download_with_thread(urls):

    # 为每个URL创建一个线程
    for url in urls:
        download_url(url)

# 测试下载性能
start_time = time.time()
download_with_thread(urls)
end_time = time.time()

print(f"Total time taken: {end_time - start_time} seconds")

# 输出内容为:
"Total time taken: 8.034498453140259 seconds"

从花费的时间可以明显的看出,多线程在处理IO密集型任务时的巨大优势。这是因为在下载网页内容主要耗时的是等待(等待服务器响应、数据传输等),线程在等待的同时会释放GIL,允许其他线程执行。通过这种方式可以让下载网页内容的操作变成并行操作,从而极大提高效率。

线程池

相比手动管理线程,使用线程池可以提高我们的管理效率,它可以实现线程复用、自动管理线程数量、任务调度。下面通过一个例子来展示线程池的使用。

使用线程池的实际案例代码:

import concurrent.futures
import time

# 定义一个函数,用于从指定的URL下载网页内容
def download_url(url):
    time.sleep(2)  # 模拟下载耗时
    return url, len(url)  # 返回URL和长度,这里只是模拟,实际上应该返回下载的内容的长度

# 定义要下载的URL列表
urls = [
    "https://www.python.org",
    "https://www.github.com",
    "https://www.stackoverflow.com",
    "https://www.reddit.com"
]

# 使用线程池来并行下载多个URL
def download_with_thread_pool(urls):
    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
        # 提交任务到线程池并返回 future 对象
        futures = [executor.submit(download_url, url) for url in urls]

        # 等待所有任务完成,并输出结果
        for future in concurrent.futures.as_completed(futures):
            url, content_length = future.result()
            print(f"Result: {url} content length: {content_length} bytes")

# 测试下载性能
start_time = time.time()
download_with_thread_pool(urls)
end_time = time.time()

print(f"Total time taken: {end_time - start_time} seconds")

# 输出内容为:
"""
Result: https://www.reddit.com content length: 22 bytes
Result: https://www.stackoverflow.com content length: 29 bytes
Result: https://www.github.com content length: 22 bytes
Result: https://www.python.org content length: 22 bytes
Total time taken: 2.0167229175567627 seconds
"""

线程池适用于大量短小任务或者IO密集型任务,如:

  • 高并发网络请求(如批量下载文件、API调用)
  • 并发数据库查询
  • 文件系统的并发读写操作

通过线程池,你可以高效地管理这些任务,确保资源得到最优的利用。

2 多进程

在这个例子中,我会给出一个多进程适用于CPU密集型任务的实际案例,通过使用multiprocessing库来并行处理多个计算密集型任务,例如计算一系列大数字的阶乘,这类任务需要大量的CPU资源。

多进程处理CPU密集型任务的实际案例代码:

import multiprocessing
import math
import time

# 定义一个函数,用于计算一个大数的阶乘
def compute_factorial(n):
    print(f"Computing factorial of {n}")
    result = math.factorial(n)
    print(f"Finished computing factorial of {n}, result: {str(result)[:10]}...")  # 只打印结果的前10位
    return result

# 使用多进程直接计算多个大数的阶乘
def compute_with_multiple_processes(numbers):
    processes = []

    # 启动多个进程
    for number in numbers:
        process = multiprocessing.Process(target=compute_factorial, args=(number,))
        processes.append(process)
        process.start()  # 启动进程

    # 等待所有进程结束
    for process in processes:
        process.join()  # 等待进程结束

# 测试多进程性能
if __name__ == "__main__":
    numbers = [50000, 60000, 70000, 80000]  # 要计算的数字列表

    start_time = time.time()
    compute_with_multiple_processes(numbers)
    end_time = time.time()

    print(f"Total time taken: {end_time - start_time} seconds")

# 输出内容为:
"""
Computing factorial of 60000
Computing factorial of 50000
Computing factorial of 80000
Computing factorial of 70000
Finished computing factorial of 50000, result: 3347320509...
Finished computing factorial of 60000, result: 1564137708...
Finished computing factorial of 70000, result: 1176812415...
Finished computing factorial of 80000, result: 3097722251...
Total time taken: 2.090242385864258 seconds
"""

单进程处理CPU密集型任务的实际案例代码:

import multiprocessing
import math
import time

# 定义一个函数,用于计算一个大数的阶乘
def compute_factorial(n):
    print(f"Computing factorial of {n}")
    result = math.factorial(n)
    print(f"Finished computing factorial of {n}, result: {str(result)[:10]}...")  # 只打印结果的前10位
    return result

# 使用单进程直接计算多个大数的阶乘
def compute_with_multiple_process(numbers):
    for number in numbers:
        result = compute_factorial(number)

if __name__ == "__main__":
    numbers = [50000, 60000, 70000, 80000]  # 要计算的数字列表

    start_time = time.time()
    compute_with_multiple_process(numbers)
    end_time = time.time()

    print(f"Total time taken: {end_time - start_time} seconds")

# 输出内容为:
"""
Computing factorial of 50000
Finished computing factorial of 50000, result: 3347320509...
Computing factorial of 60000
Finished computing factorial of 60000, result: 1564137708...
Computing factorial of 70000
Finished computing factorial of 70000, result: 1176812415...
Computing factorial of 80000
Finished computing factorial of 80000, result: 3097722251...
Total time taken: 4.4995503425598145 seconds
"""

可以看出使用多进程策略可以有效提升CPU密集型任务的执行效率,其实这是很好理解的,因为每个进程拥有独立的Python解释器和内存空间,因此不会受到GIL的限制,每个进程能够独立运行并利用多个核心进行并行计算

进程池

同线程池一样,多进程也可以通过进程池来提高多个进程的管理效率,这里给出简单的代码示例,不再赘述。

使用进程池的实际案例代码:

import multiprocessing
import math
import time

# 定义一个函数,用于计算一个大数的阶乘
def compute_factorial(n):
    print(f"Computing factorial of {n}")
    result = math.factorial(n)
    print(f"Finished computing factorial of {n}")
    return result

# 定义要计算的数值列表
numbers = [50000, 60000, 70000, 80000]

# 使用多进程并行计算多个大数的阶乘
def compute_with_multiprocessing(numbers):
    with multiprocessing.Pool(processes=4) as pool:  # 创建一个包含4个进程的进程池
        results = pool.map(compute_factorial, numbers)  # 并行执行多个任务
    return results

# 测试多进程性能
if __name__ == "__main__":
    start_time = time.time()
    results = compute_with_multiprocessing(numbers)
    end_time = time.time()

    print(f"Total time taken: {end_time - start_time} seconds")

# 输出内容为:
"""
Computing factorial of 50000
Computing factorial of 60000
Computing factorial of 70000
Computing factorial of 80000
Finished computing factorial of 50000
Finished computing factorial of 60000
Finished computing factorial of 70000
Finished computing factorial of 80000
Total time taken: 0.456223726272583 seconds
"""

多进程之间共享内存

这个例子展示了如何在Python的多进程中使用共享内存来安全地在多个进程之间共享数据,并通过锁机制来确保数据的完整性。这种模式适用于需要并发操作但需要保证共享数据一致性的场景。

多进程共享内存的案例代码:

import multiprocessing
import time

# 定义一个函数,让每个进程修改共享内存中的值
def increment_value(shared_value, lock):
    for _ in range(5):
        time.sleep(0.5)  # 模拟一些耗时的操作
        with lock:  # 获取锁,以确保对共享数据的同步操作
            shared_value.value += 1
            print(f"Process {multiprocessing.current_process().name} incremented value to {shared_value.value}")

# 主程序
if __name__ == "__main__":
    # 创建一个共享内存的整数对象,初始值为0
    shared_value = multiprocessing.Value('i', 0)  # 'i' 表示共享的整数类型
    lock = multiprocessing.Lock()  # 创建一个锁,用于保证并发安全

    # 创建两个进程,分别执行increment_value函数
    process1 = multiprocessing.Process(target=increment_value, args=(shared_value, lock), name="Process-1")
    process2 = multiprocessing.Process(target=increment_value, args=(shared_value, lock), name="Process-2")

    # 启动两个进程
    process1.start()
    process2.start()

    # 等待两个进程执行完成
    process1.join()
    process2.join()

    # 输出最终结果
    print(f"Final shared value: {shared_value.value}")

# 输出内容为:
"""
Process Process-1 incremented value to 1
Process Process-2 incremented value to 2
Process Process-1 incremented value to 3
Process Process-2 incremented value to 4
Process Process-2 incremented value to 5
Process Process-1 incremented value to 6
Process Process-1 incremented value to 7
Process Process-2 incremented value to 8
Process Process-1 incremented value to 9
Process Process-2 incremented value to 10
Final shared value: 10
"""

上面的例子使用multiprocessing.Value来共享整数值, 我们还可以通过multiprocessing.Array共享数组,用法类似。此外,如果想共享更复杂的对象,我们可以使用multiprocessing.Manager方法,Manager 允许我们共享任意 Python 对象,感兴趣的可自行学习相关使用方法。

3 协程

协程是能够在执行过程中暂停和恢复的一种函数,在这个例子中,我以异步编程为例进行讲解。异步编程是一种在等待I/O操作(如文件读取、网络请求等)时不阻塞主线程的方式,使得程序能够在等待操作完成的同时继续执行其他任务。Python 的 asyncio 模块是实现异步编程的核心工具,它允许我们编写高效的 I/O 密集型程序。

异步编程案例代码:

import asyncio
import time

# 定义一个异步函数,模拟网络请求
async def fetch_data(id, delay):
    print(f"Task {id}: Starting network request, waiting for {delay} seconds...")
    await asyncio.sleep(delay)  # 模拟I/O操作,等待一定时间
    print(f"Task {id}: Network request completed!")
    return f"Result from task {id}"

# 定义一个主函数来运行多个异步任务
async def main():
    print("Starting all tasks...")

    # 创建多个任务,并发执行
    # create_task()会把创建的任务注册到event loop中,也就是告诉event loop这个task可以执行了。这样在执行tasks的时候event loop就知道了有三个任务,当其中的一个需要等待,那event loop就会执行其他的任务,这样就不会阻塞程序的执行了
    tasks = [
        asyncio.create_task(fetch_data(1, 2)),  # 任务1,等待2秒
        asyncio.create_task(fetch_data(2, 4)),  # 任务2,等待4秒
        asyncio.create_task(fetch_data(3, 1)),  # 任务3,等待1秒
    ]

    # 等待所有任务完成并获取返回值
    results = await asyncio.gather(*tasks)

    print("All tasks completed!")
    print("Results:", results)

# 运行主函数
if __name__ == "__main__":
    start_time = time.time()  # 记录开始时间
    asyncio.run(main())       # 运行异步任务。首先建立event loop,然后执行异步函数,也就是执行task
    end_time = time.time()    # 记录结束时间
    print(f"Total time taken: {end_time - start_time:.2f} seconds")

# 输出结果为:
"""
Starting all tasks...
Task 1: Starting network request, waiting for 2 seconds...
Task 2: Starting network request, waiting for 4 seconds...
Task 3: Starting network request, waiting for 1 seconds...
Task 3: Network request completed!
Task 1: Network request completed!
Task 2: Network request completed!
All tasks completed!
Results: ['Result from task 1', 'Result from task 2', 'Result from task 3']
Total time taken: 4.01 seconds
"""

代码解释:

  • async def 用来定义一个异步函数,意味着这个函数可以异步运行。
  • await 用于等待异步操作完成,比如在 fetch_data 函数中,await asyncio.sleep(delay) 模拟了网络请求的等待,这个操作不会阻塞整个程序的执行。
  • create_task() 创建一个异步任务,并允许它们并发执行。这意味着多个任务可以同时开始执行,而不需要一个任务完成后再启动下一个任务。
  • gather() 用来并行运行多个异步任务,并收集它们的结果。await asyncio.gather(*tasks) 等待所有任务执行完毕,然后返回所有任务的结果。

运行结果解释:

  • 任务 1 需要 2 秒,任务 2 需要 4 秒,任务 3 需要 1 秒。
  • 所有任务是并发运行的,程序并没有等待某一个任务完成再开始下一个任务。
  • 因为任务 2 的等待时间最长(4 秒),所以整个程序的总执行时间就是 4 秒,即最长的任务时间。
  • 最后输出的结果是按照tasks的顺序进行输出的。

4 子进程

在Python中,subprocess(子进程)模块用于启动并管理子进程,可以让你执行系统命令或其他外部程序,并与这些程序进行交互。subprocess比传统的os.system()更强大,因为它能够捕获输出、处理标准输入输出以及管理进程的执行状态。

下面是一个简单的subprocess使用案例,它演示了如何使用subprocess.run()来执行系统命令并捕获其输出。

subprocess 使用案例代码:

import subprocess

# 使用subprocess运行系统命令 'ls' (Linux/macOS)列出当前目录的内容
def list_directory():
    try:
        # 使用 'subprocess.run' 来执行命令并捕获输出
        result = subprocess.run(['ls', '-l'], capture_output=True, text=True, check=True)

        # 打印子进程的标准输出
        print("Command output:")
        print(result.stdout)

        # 打印子进程的标准错误(如果有)
        if result.stderr:
            print("Error output:")
            print(result.stderr)
    except subprocess.CalledProcessError as e:
        # 如果命令执行失败,捕获异常并打印错误
        print(f"An error occurred: {e}")

# 调用函数
if __name__ == "__main__":
    list_directory()

# 输出内容为:
"""
Command output:
total 52
drwxr-xr-x 12 root root  4096 Jul 24 09:16 JHQ
drwxrwxrwx 15 root root  4096 Sep  8 22:18 chenkj
drwxr-xr-x  2 root root  4096 May 23 10:47 ckj
drwxrwxrwx  9 root root  4096 May 10 09:58 fin-data
drwx------  2 root root 16384 Nov  4  2023 lost+found
drwxrwxrwx  5 root root  4096 Sep  2 17:12 myp
drwxr-xr-x 34 root root  4096 Nov 28  2023 netdata
drwxrwxrwx 27 root root  4096 Sep  4 11:27 stephen
-rw-rw-r--  1 tu   tu     781 Sep  8 22:17 ls_l.py
drwxr-xr-x  3 ws   ws    4096 Apr 24 23:12 ws
"""

代码解释:

  • subprocess.run() 是用来执行外部命令的一个函数。它接收命令作为参数列表,比如在上面的例子中,[‘ls’, ‘-l’] 是我们传递给 subprocess.run() 的命令,这里是 ls -l,用于列出当前目录下的文件(适用于Linux和macOS)。
  • capture_output=True: 捕获标准输出和标准错误。这样我们可以通过 result.stdout 和 result.stderr 来访问输出结果。result.stdout代表子进程的标准输出,即命令执行成功时输出的结果。result.stderr代表子进程的标准错误输出,即命令执行过程中产生的错误信息。
  • text=True: 将输出捕获为字符串(文本),而不是字节类型。
  • check=True: 如果命令执行返回非零退出状态,抛出 CalledProcessError 异常。

5 一些思考和理解

  1. 多进程可以用在IO密集型的任务中吗?
    因为多进程本身就是多个进程同时执行,不存在GIL的限制。那为什么IO密集型任务不使用多进程呢,这样也可以实现并行操作。这是因为进程的创建和上下文切换开销较大,内存占用与资源共享也都大大受限,在IO密集型场景中引入了不必要的开销和复杂性,反而无法充分提升效率。
  2. 多线程和异步编程都适用于IO密集型场景,那他们有什么区别和联系吗?
    区别:
    异步编程是单线程、多任务、非阻塞的,更适合处理大量I/O密集型任务,并且在Python中能避开线程上下文切换的开销,特别适合网络应用、大量数据库查询等场景。
    多线程编程是多线程、多任务,阻塞的,需要创建和管理线程,受GIL的影响。适用于需要并发处理I/O操作且任务数量不多的情况,同时也适合不受GIL限制的外部程序调用和执行(比如某些计算任务或外部进程)。
    联系:
    都能提高并发性,异步编程和多线程编程的主要目标都是提高并发性,避免程序因为I/O操作而被阻塞。
  3. 如何应用?
    并发执行总的来说可以分为两类,CPU密集型和IO密集型。在实际的场景中,我们要分析具体影响服务性能的瓶颈是什么,然后采用对应的方法进行解决。比如我之前写过一篇文章介绍模型在推理时遇到了瓶颈,这就是典型的CPU密集型任务,当时采用了多进程方式来提升服务的并发。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值