百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术流 > 正文

5分钟了解 Python 中的多线程和多处理

citgpt 2024-09-09 02:16 10 浏览 0 评论

在编写需要同时执行多个任务的程序时,有两种强大的技术可以提供帮助:多线程和多处理。这些方法可以显著提高应用程序的性能和效率,但它们各有优势、劣势和合适的用例。

了解 Python 中的全局解释器锁 (GIL)

在深入研究多线程和多处理之前,了解 Python 中的全局解释器锁 (GIL) 至关重要。全局解释器锁 (GIL) 是 CPython 实现(标准 Python 解释器)的关键组件,对多线程 Python 程序具有重大影响。了解 Python 使用 GIL 的原因有助于阐明其对性能和并发性的影响。

5分钟了解 Python 中的多线程和多处理

什么是 GIL?

全局解释器锁是一种互斥锁,用于保护对 Python 对象的访问。它确保一次只有一个线程执行 Python 字节码。此锁是必要的,因为 Python 的内存管理不是线程安全的。如果没有 GIL,从多个线程同时访问 Python 对象可能会导致数据不一致或损坏。

为什么 Python 使用 GIL?

  • 简化内存管理:Python 的内存管理,尤其是垃圾回收的引用计数,不是线程安全的。GIL 确保内存管理操作(如递增或递减引用计数)是原子的,并且不受竞争条件的影响。
    通过使用 GIL,Python 解释器避免了与线程安全内存管理相关的复杂性和潜在错误。
  • 易于与 C 库集成:Python 通常用作与 C 库交互的脚本语言。许多 C 库不是线程安全的。GIL 提供了一种简单的方法来确保 Python 与这些库的交互保持安全和一致。
    它还简化了 C 扩展的集成,因为开发人员不必担心他们的代码线程安全。
  • 历史背景:GIL 是在 Python 历史的早期引入的,当时该语言的主要用例并不涉及繁重的多线程。当时,拥有 GIL 的简单性和性能优势超过了缺点。
    删除 GIL 需要对 Python 的内存管理和垃圾回收系统进行重大的重新设计。

GIL的影响

GIL 的影响在 CPU 密集型多线程程序中最为明显。下面是一个演示这种影响的示例:

import threading
import time


def cpu_bound_task():
    count = 0
    for i in range(10 ** 7):
        count += 1
    print(f"Task completed with count = {count}")


# Measuring time for single execution run twice sequentially
start_time = time.time()
cpu_bound_task()
cpu_bound_task()
print(f"Single execution duration (run twice): {time.time() - start_time:.2f} seconds")

# Measuring time for two threads running the task concurrently
thread1 = threading.Thread(target=cpu_bound_task)
thread2 = threading.Thread(target=cpu_bound_task)

start_time = time.time()
thread1.start()
thread2.start()

thread1.join()
thread2.join()
print(f"Two threads duration: {time.time() - start_time:.2f} seconds")

在此示例中,在第一部分中依次执行两次, cpu_bound_task 然后在第二部分中同时使用两个线程。尽管使用了两个线程,但由于 GIL 的原因,线程的总执行时间将大致相同或略差于连续运行两次任务。

多线程

多线程涉及在单个进程中运行多个线程。每个线程独立运行,但共享相同的内存空间,因此对于涉及大量等待的任务非常有用,例如 I/O 操作(读取和写入文件以及处理网络请求)。

何时使用多线程:

  • 当程序涉及 I/O 绑定任务时,例如读取或写入文件、网络通信或数据库操作。
  • 当任务可以并发运行且不占用 CPU 时。
  • 当应用程序需要维护共享状态或内存时。

优点:

  • 对于 I/O 密集型任务非常有效,在这些任务中,CPU 可能处于空闲状态,等待外部操作完成。
  • 与多处理相比,开销更低,因为线程共享相同的内存空间。
  • 由于共享内存,线程间通信更容易。

缺点:

  • Python 中的 GIL 会降低 CPU 密集型任务的性能,从而阻止真正的并行性。
  • 由于潜在的竞争条件和死锁,调试可能具有挑战性。
  • 如果管理不当,共享内存可能会导致问题。

下面是在 Python 中使用多线程的详细示例:

import threading
import time


def print_numbers():
    for i in range(10):
        print(f"Number: {i}")
        time.sleep(1)


def print_letters():
    for letter in 'abcdefghij':
        print(f"Letter: {letter}")
        time.sleep(1)


if __name__ == '__main__':
    # Creating threads
    thread1 = threading.Thread(target=print_numbers)
    thread2 = threading.Thread(target=print_letters)

    # Starting threads
    thread1.start()
    thread2.start()

    # Waiting for threads to complete
    thread1.join()
    thread2.join()

    print("All tasks completed!")

在此示例中,创建了两个线程以同时打印数字和字母。两个线程共享相同的内存空间并同时运行。

threading - 基于线程的并行性

多进程

多进程涉及运行多个进程,每个进程都有自己的内存空间。此技术对于主要限制是 CPU 处理能力的 CPU 密集型任务特别有用。每个进程都独立运行,允许真正的并行性,尤其是在多核系统上。

何时使用多进程:

  • 用于 CPU 密集型任务,例如数学计算、数据处理或任何需要大量 CPU 资源的操作。
  • 当任务需要真正并行时。
  • 当任务的单独内存空间是有益的,避免共享内存问题时。

优点:

  • 真正的并行性对于 CPU 密集型任务特别有用,因为每个进程都可以在单独的内核上运行。
  • 每个进程都有自己的内存空间,从而降低了内存损坏的风险。
  • 在多核系统上具有更好的性能。

缺点:

  • 由于创建单独的流程,开销更高。
  • 与线程相比,进程间通信 (IPC) 更复杂。
  • 由于每个进程都有自己的内存空间,因此增加了内存使用量。

下面是在 Python 中使用多处理的详细示例:

import multiprocessing
import time


def print_numbers():
    for i in range(10):
        print(f"Number: {i}")
        time.sleep(1)


def print_letters():
    for letter in 'abcdefghij':
        print(f"Letter: {letter}")
        time.sleep(1)


if __name__ == '__main__':
    # Creating processes
    process1 = multiprocessing.Process(target=print_numbers)
    process2 = multiprocessing.Process(target=print_letters)

    # Starting processes
    process1.start()
    process2.start()

    # Waiting for processes to complete
    process1.join()
    process2.join()

    print("All tasks completed!")

在此示例中,创建了两个进程来同时打印数字和字母。每个进程都使用自己的内存空间独立运行,确保真正的并行执行。

主要区别

  • 内存共享:线程共享相同的内存空间,使通信更容易,但存在内存损坏的风险。进程具有单独的内存空间,使其更安全,但需要更多内存。
  • GIL 限制:Python 的 GIL 通过阻止 CPU 密集型任务中的真正并行性来影响多线程。多处理绕过 GIL,允许真正的并行执行。
  • 开销:由于共享内存,线程的开销较低,而进程的开销较高,因为它们需要单独的内存空间。

在它们之间做出选择

  • 将多线程用于 I/O 绑定任务,在这些任务中,程序会花费大量时间等待外部操作完成。
  • 对 CPU 密集型任务使用多扣女,其目标是跨多个内核充分利用 CPU。

比较多线程和多进程

为了了解 Python 中多线程和多处理之间的区别,特别是对于 CPU 密集型任务,我们使用 10 个线程和 10 个进程实现并比较了这两种方法。以下是运行这些脚本的示例和关键要点。

用于比较的任务涉及执行大量迭代的简单循环,模拟 CPU 密集型操作。

def cpu_bound_task():
    count = 0
    for i in range(10**7):
        count += 1
    return count

多线程示例

在多线程示例中,创建了 10 个线程来并发运行 CPU 密集型任务。

import threading
import time


def cpu_bound_task():
    count = 0
    for i in range(10 ** 7):
        count += 1
    return count


def thread_task():
    result = cpu_bound_task()
    print(f"Task completed with count = {result}")


if __name__ == '__main__':
    start_time = time.time()

    # Creating 10 threads
    threads = []
    for _ in range(10):
        thread = threading.Thread(target=thread_task)
        threads.append(thread)
        thread.start()

    # Waiting for all threads to complete
    for thread in threads:
        thread.join()

    print(f"Multithreading duration: {time.time() - start_time:.2f} seconds")

输出:

Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Multithreading duration: 2.53 seconds

多进程示例

在多进程示例中,我们创建了 10 个进程来并行运行 CPU 密集型任务。

import multiprocessing
import time


def cpu_bound_task():
    count = 0
    for i in range(10 ** 7):
        count += 1
    return count


def process_task():
    result = cpu_bound_task()
    print(f"Task completed with count = {result}")


if __name__ == '__main__':
    start_time = time.time()

    # Creating 10 processes
    processes = []
    for _ in range(10):
        process = multiprocessing.Process(target=process_task)
        processes.append(process)
        process.start()

    # Waiting for all processes to complete
    for process in processes:
        process.join()

    print(f"Multiprocessing duration: {time.time() - start_time:.2f} seconds")

输出:

Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Task completed with count = 10000000
Multiprocessing duration: 0.78 seconds

结果摘要

对 CPU 密集型任务的多线程和多处理进行比较,结果如下:

  • 多处理持续时间:0.78 秒
  • 多线程持续时间:2.53 秒

主要见解:

  • 对于受 CPU 限制的任务,多处理的性能明显优于多线程,在不到三分之一的时间内完成。
  • 全局解释器锁 (GIL) 限制了 Python 中多线程对 CPU 密集型操作的有效性,因为它阻止了线程的真正并行执行。
  • 多处理通过运行单独的进程来利用多个 CPU 内核,每个进程都有自己的内存空间和 GIL,从而实现真正的并行性和 CPU 资源的高效利用。

这些结果清楚地表明,对于 CPU 密集型任务,与多线程相比,多处理是 Python 中更有效的方法。

多线程什么时候更好?

多线程在任务受 I/O 限制而不是受 CPU 限制的情况下特别有效。I/O 绑定任务涉及花费大部分时间等待外部资源(如文件 I/O、网络 I/O 或数据库查询)而不是使用 CPU 的操作。在这些情况下,全局解释器锁 (GIL) 不是瓶颈,因为 CPU 经常处于空闲状态,等待 I/O 操作完成。

下面是多线程有益的情况示例:

想象一下,需要从多个网站抓取数据。对网站的每个请求都涉及网络 I/O,这比 CPU 处理时间慢得多。使用多线程允许您同时启动多个网络请求,从而有效地利用等待时间。

import threading
import requests
import time

# List of URLs to scrape
urls = [
    "http://example.com",
    "http://example.org",
    "http://example.net",
    # Add more URLs as needed
]


def fetch_url(url: str):
    try:
        response = requests.get(url)
        print(f"Fetched {url} with status: {response.status_code}")
    except requests.RequestException as e:
        print(f"Error fetching {url}: {e}")


def fetch_all_urls():
    threads = []
    for url in urls:
        thread = threading.Thread(target=fetch_url, args=(url,))
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()


if __name__ == '__main__':
    start_time = time.time()
    fetch_all_urls()
    print(f"Multithreading duration: {time.time() - start_time:.2f} seconds")

在此示例中,定义了一个 URL 列表以从多个网站抓取数据。该 fetch_url 函数向给定 URL 发出 HTTP GET 请求,并打印响应的状态代码。如果在请求过程中出现错误,它将捕获异常并打印错误消息。

fetch_all_urls 函数为列表中的每个 URL 创建一个线程。它启动所有线程,然后等待每个线程使用 join() 完成。这允许同时启动和处理所有网络请求。

在主执行块中,脚本测量使用多线程并发获取所有 URL 所花费的时间。通过运行该 fetch_all_urls 函数,与按顺序发出请求相比,从所有 URL 获取数据所需的总时间大大减少。

此示例突出显示了 I/O 绑定任务的多线程处理效率,在这些任务中,程序会花费大量时间等待外部操作,例如网络响应。通过发出并发请求,可以最大程度地减少整体执行时间,从而展示了多线程在此类场景中的优势。

多线程和多进程的替代方案

虽然多线程和多处理是 Python 中实现并发的常用技术,但根据任务的性质,也可以使用其他一些方法和库。以下是一些详细的替代方法,包括说明和代码示例。

异步(异步 I/O)

Asyncio 是一个使用 async/await 语法编写并发代码的库。它主要用于 I/O 绑定任务,在这些任务中,程序需要处理多个连接或同时执行多个 I/O 操作,而不会阻塞主线程。

优点:

  • 适用于 I/O 绑定任务。
  • 不需要多个线程或进程,避免了与之相关的开销。
  • 在内存和 CPU 使用率方面可以更有效率。

缺点:

  • 需要不同的编程模型 (async/await),理解和实现起来可能更复杂。

例:

import asyncio


async def print_numbers():
    for i in range(10):
        print(f"Number: {i}")
        await asyncio.sleep(1)  # Non-blocking sleep


async def print_letters():
    for letter in 'abcdefghij':
        print(f"Letter: {letter}")
        await asyncio.sleep(1)  # Non-blocking sleep


async def main():
    await asyncio.gather(print_numbers(), print_letters())  # Run both coroutines concurrently


# Run the main coroutine
asyncio.run(main())

在此示例中, print_numbersprint_letters 是异步函数(协程)。这是一种 await asyncio.sleep(1) 非阻塞睡眠,允许其他任务在等待时运行。该 asyncio.gather 函数同时运行多个协程,并 asyncio.run(main()) 运行执行任务的主协程。

并发期货

concurrent.futures 模块提供了一个高级接口,用于使用线程或进程异步执行可调用对象。

优点:

  • 通过高级界面简化线程和进程的使用。
  • 抽象出线程和进程管理的低级细节。

缺点:

  • 线程仍受 GIL 的影响,进程的开销较高。

例:

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time


def print_numbers():
    for i in range(10):
        print(f"Number: {i}")
        time.sleep(1)


def print_letters():
    for letter in 'abcdefghij':
        print(f"Letter: {letter}")
        time.sleep(1)


# Using ThreadPoolExecutor
with ThreadPoolExecutor() as executor:
    futures = [executor.submit(print_numbers), executor.submit(print_letters)]
    for future in futures:
        future.result()

print("All tasks completed with ThreadPoolExecutor")

# Using ProcessPoolExecutor
with ProcessPoolExecutor() as executor:
    futures = [executor.submit(print_numbers), executor.submit(print_letters)]
    for future in futures:
        future.result()

print("All tasks completed with ProcessPoolExecutor")

在此示例中,管理 ThreadPoolExecutor 用于执行任务的线程池,其中 executor.submit(print_numbers) 计划任务在线程中运行,并 future.result() 等待任务完成并检索结果。同样,管理 ProcessPoolExecutor 用于执行任务的进程池。

相关推荐

js中arguments详解

一、简介了解arguments这个对象之前先来认识一下javascript的一些功能:其实Javascript并没有重载函数的功能,但是Arguments对象能够模拟重载。Javascrip中每个函数...

firewall-cmd 常用命令

目录firewalldzone说明firewallzone内容说明firewall-cmd常用参数firewall-cmd常用命令常用命令 回到顶部firewalldzone...

epel-release 是什么

EPEL-release(ExtraPackagesforEnterpriseLinux)是一个软件仓库,它为企业级Linux发行版(如CentOS、RHEL等)提供额外的软件包。以下是关于E...

FullGC详解  什么是 JVM 的 GC
FullGC详解 什么是 JVM 的 GC

前言:背景:一、什么是JVM的GC?JVM(JavaVirtualMachine)。JVM是Java程序的虚拟机,是一种实现Java语言的解...

2024-10-26 08:50 citgpt

使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
跨域(CrossOrigin)

1.介绍  1)跨域问题:跨域问题是在网络中,当一个网络的运行脚本(通常时JavaScript)试图访问另一个网络的资源时,如果这两个网络的端口、协议和域名不一致时就会出现跨域问题。    通俗讲...

微服务架构和分布式架构的区别

1、含义不同微服务架构:微服务架构风格是一种将一个单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间通信采用轻量级通信机制(通常用HTTP资源API)。这些服务围绕业务能力构建并...

深入理解与应用CSS clip-path 属性
深入理解与应用CSS clip-path 属性

clip-pathclip-path是什么clip-path 是一个CSS属性,允许开发者创建一个剪切区域,从而决定元素的哪些部分可见,哪些部分会被隐...

2024-10-25 11:51 citgpt

HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
Request.ServerVariables 大全

Request.ServerVariables("Url")返回服务器地址Request.ServerVariables("Path_Info")客户端提供的路...

python操作Kafka

目录一、python操作kafka1.python使用kafka生产者2.python使用kafka消费者3.使用docker中的kafka二、python操作kafka细...

Runtime.getRuntime().exec详解

Runtime.getRuntime().exec详解概述Runtime.getRuntime().exec用于调用外部可执行程序或系统命令,并重定向外部程序的标准输入、标准输出和标准错误到缓冲池。...

promise.all详解 promise.all是干什么的
promise.all详解 promise.all是干什么的

promise.all详解promise.all中所有的请求成功了,走.then(),在.then()中能得到一个数组,数组中是每个请求resolve抛出的结果...

2024-10-24 16:21 citgpt

Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解

取消回复欢迎 发表评论: