Press "Enter" to skip to content

Python中的多线程和多进程介绍

Python中的多线程和多进程介绍 四海 第1张 

本教程将讨论利用Python执行多线程和多编程任务的能力。它们可以在单个进程或多个进程之间执行并发操作。并行和并发执行提高了系统的速度和效率。在讨论了多线程和多程序的基础知识之后,我们还将讨论使用Python库实现它们的实际应用。首先我们简要讨论并行系统的好处。

  1. 改进的性能:通过并发执行任务,我们可以减少执行时间,提高系统的整体性能。
  2. 可扩展性:我们可以将一个大任务分割成多个较小的子任务,并为它们分配独立的核心或线程进行独立执行。这在大规模系统中非常有帮助。
  3. 高效的I/O操作:通过并发,CPU无需等待进程完成其I/O操作。CPU可以立即开始执行下一个进程,直到先前的进程忙于其I/O操作。
  4. 资源优化:通过分配资源,我们可以防止单个进程占用所有资源。这可以避免小进程出现“饥饿”的问题。

 Python中的多线程和多进程介绍 四海 第2张 

这些是一些通常需要并发或并行执行的常见原因。现在,让我们回到主要的主题,即多线程和多程序,并讨论它们的主要区别。

 

什么是多线程?

 

多线程是实现单个进程中并行性的一种方式,能够执行同时的任务。多个线程可以在单个进程内创建并在该进程内并行执行较小的任务。

存在于单个进程中的线程共享一个公共的内存空间,但它们的堆栈和寄存器是独立的。由于共享内存,它们的计算成本较低。

 Python中的多线程和多进程介绍 四海 第3张 

多线程主要用于执行I/O操作,即如果程序的某个部分正在忙于I/O操作,那么剩余的程序可以保持响应。然而,在Python的实现中,由于全局解释器锁(GIL),多线程无法实现真正的并行性。

简而言之,GIL是一个互斥锁,只允许一个线程与Python字节码交互,即使在多线程模式下,也只有一个线程可以同时执行字节码。

这样做是为了在CPython中保持线程安全,但这限制了多线程的性能优势。为了解决这个问题,Python有一个单独的多进程库,我们之后会讨论。

什么是守护线程?

不断在后台运行的线程称为守护线程。它们的主要工作是支持主线程或非守护线程。守护线程不会阻塞主线程的执行,即使它已经完成了其执行。

在Python中,守护线程主要用作垃圾收集器。它将销毁所有无用的对象并默认释放内存,以便主线程可以正常使用和执行。

 

什么是多进程?

 

多进程用于执行多个进程的并行执行。它帮助我们实现真正的并行性,因为我们同时执行独立的进程,它们具有自己的内存空间。它使用CPU的独立核心,并且在执行进程间进行进程间通信以交换数据。

与多线程相比,多进程更加计算密集,因为我们没有使用共享的内存空间。但它允许我们进行独立的执行,并克服了全局解释器锁的限制。

 Python中的多线程和多进程介绍 四海 第4张 

上图展示了一个多进程环境,主进程创建了两个独立的进程,并为它们分配了不同的任务。

多线程实现

现在是时候使用Python来实现一个基本的多线程示例了。Python有一个内置的threading模块用于多线程实现。

  1. 导入库:
import threadingimport os

  1. 计算平方的函数:

这是一个简单的函数,用于找到数字的平方。它以一个数字列表作为输入,并将列表中每个数字的平方以及使用的线程名称和与该线程关联的进程ID输出。

def calculate_squares(numbers):    for num in numbers:        square = num * num        print(            f"数字{num}的平方为{square} | 线程名称{threading.current_thread().name} | 进程ID{os.getpid()}"        )

  1. 主要函数:

我们有一个数字列表,我们将等分该列表,并将它们分别命名为first_halfsecond_half。现在我们将为这些列表分别指定两个单独的线程t1t2

Thread函数创建一个新线程,该线程接受一个带有参数列表的函数作为输入。您还可以为线程指定一个单独的名称。

.start()函数将开始执行这些线程,.join()函数将阻塞主线程的执行,直到给定的线程完全执行完毕。

if __name__ == "__main__":    numbers = [1, 2, 3, 4, 5, 6, 7, 8]    half = len(numbers) // 2    first_half = numbers[:half]    second_half = numbers[half:]    t1 = threading.Thread(target=calculate_squares, name="t1", args=(first_half,))    t2 = threading.Thread(target=calculate_squares, name="t2", args=(second_half,))    t1.start()    t2.start()    t1.join()    t2.join()

输出结果:

数字1的平方为1 | 线程名称t1 | 进程ID345数字2的平方为4 | 线程名称t1 | 进程ID345数字5的平方为25 | 线程名称t2 | 进程ID345数字3的平方为9 | 线程名称t1 | 进程ID345数字6的平方为36 | 线程名称t2 | 进程ID345数字4的平方为16 | 线程名称t1 | 进程ID345数字7的平方为49 | 线程名称t2 | 进程ID345数字8的平方为64 | 线程名称t2 | 进程ID345

注意:以上创建的所有线程都是非守护线程。要创建一个守护线程,您需要编写t1.setDaemon(True)来使线程t1成为守护线程。

现在,我们将理解上述代码生成的输出。我们可以观察到进程ID(即PID)对于两个线程都是相同的,这意味着这两个线程是同一个进程的一部分。

您还可以观察到输出结果不是按顺序生成的。在第一行中,您将看到由线程1生成的输出,然后在第3行中,是线程2生成的输出,然后再次是线程1在第四行。这清楚地表明这些线程同时工作。

并行并不意味着这两个线程同时执行,因为一次只有一个线程在执行。它不会减少执行时间。CPU开始执行一个线程,但中途将其放下并转到另一个线程,一段时间后,回到主线程并从上次离开的地方开始执行。

多进程实现

希望你对多线程及其实现和限制有基本了解。现在,是时候学习多进程实现以及我们如何克服这些限制了。

我们将按照相同的例子进行,但是不再创建两个独立的线程,而是创建两个独立的进程并进行讨论。

  1. 导入库:
from multiprocessing import Processimport os

我们将使用multiprocessing模块创建独立的进程。

  1. 计算平方的函数:

该函数将保持不变。我们只是删除了打印线程信息的语句。

def calculate_squares(numbers):    for num in numbers:        square = num * num        print(            f"数字 {num} 的平方是 {square} | 进程的PID {os.getpid()}"        )

  1. 主函数:

主函数有一些修改。我们只是创建了一个单独的进程而不是线程。

if __name__ == "__main__":    numbers = [1, 2, 3, 4, 5, 6, 7, 8]    half = len(numbers) // 2    first_half = numbers[:half]    second_half = numbers[half:]    p1 = Process(target=calculate_squares, args=(first_half,))    p2 = Process(target=calculate_squares, args=(second_half,))    p1.start()    p2.start()    p1.join()    p2.join()

输出:

数字 1 的平方是 1 | 进程的PID 1125数字 2 的平方是 4 | 进程的PID 1125数字 3 的平方是 9 | 进程的PID 1125数字 4 的平方是 16 | 进程的PID 1125数字 5 的平方是 25 | 进程的PID 1126数字 6 的平方是 36 | 进程的PID 1126数字 7 的平方是 49 | 进程的PID 1126数字 8 的平方是 64 | 进程的PID 1126

我们观察到每个列表都由一个独立的进程执行。两者具有不同的进程ID。为了检查我们的进程是否已并行执行,我们需要创建一个单独的环境,下面我们将讨论这个。

使用和不使用多进程进行运行时间计算

为了检查我们是否获得真正的并行性,我们将计算算法在使用多进程和不使用多进程的情况下的运行时间。

为此,我们需要一个包含超过10^6个整数的大型整数列表。我们可以使用random库生成一个列表。我们将使用Python的time模块来计算运行时间。下面是此的实现。代码是很直观的,但你仍然可以查看代码注释。

from multiprocessing import Processimport osimport timeimport randomdef calculate_squares(numbers):    for num in numbers:        square = num * numif __name__ == "__main__":    numbers = [        random.randrange(1, 50, 1) for i in range(10000000)    ]  # 创建一个包含10^7大小的随机整数列表。    half = len(numbers) // 2    first_half = numbers[:half]    second_half = numbers[half:]    # ----------------- 创建单进程环境 ------------------------#    start_time = time.time()  # 不使用多进程的开始时间    p1 = Process(        target=calculate_squares, args=(numbers,)    )  # 单个进程P1执行整个列表    p1.start()    p1.join()    end_time = time.time()  # 不使用多进程的结束时间    print(f"不使用多进程的执行时间:{(end_time-start_time)*10**3}ms")    # ----------------- 创建多进程环境 ------------------------#    start_time = time.time()  # 使用多进程的开始时间    p2 = Process(target=calculate_squares, args=(first_half,))    p3 = Process(target=calculate_squares, args=(second_half,))    p2.start()    p3.start()    p2.join()    p3.join()    end_time = time.time()  # 使用多进程的结束时间    print(f"使用多进程的执行时间:{(end_time-start_time)*10**3}ms")

 

输出:

无多进程执行时间:619.8039054870605毫秒
使用多进程的执行时间:321.70287895202637毫秒

 

您可以观察到使用多进程的时间几乎是不使用多进程的一半。这表明这两个过程在同一时间内同时执行,并呈现了真正的并行行为。

您还可以阅读来自VoAGI的文章Sequential vs Concurrent vs Parallelism,这将帮助您了解这些Sequential、Concurrent和Parallel processes之间的基本区别。

[Aryan Garg](https://www.linkedin.com/in/aryan-garg-1bbb791a3/)是一名电气工程学士学位的学生,目前是本科的最后一年。他对Web开发和机器学习领域感兴趣。他一直追求这个兴趣,并渴望在这些方向上做更多的工作。

Leave a Reply

Your email address will not be published. Required fields are marked *