Python中的多线程和多进程:概念、对比和实践指南

2023年12月05日 由 neo 发表 275 0

QQ截图20231205141838

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

  1. 性能提升:有了同时执行任务的能力,我们可以减少执行时间,提高系统的整体性能。
  2. 可扩展性:我们可以将一个大任务分解为各种较小的子任务,并为它们分配一个单独的核心或线程来进行独立的执行。这对于大规模的系统是有帮助的。
  3. 高效的I/O操作:借助并发,CPU不必等待一个进程完成它的I/O操作。CPU可以立即开始执行下一个进程,直到前一个进程忙于它的I/O。
  4. 资源优化:通过分配资源,我们可以防止一个进程占用所有的资源。这可以避免较小进程的饥饿问题。

QQ截图20231205142102

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

什么是多线程?

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

在单个进程内的线程共享一个公共的内存空间,但它们的堆栈跟踪和寄存器是分开的。由于这种共享内存,它们的计算成本较低。

QQ截图20231205142221

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

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

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

什么是守护线程?

那些不断在后台运行的线程被称为守护线程。它们的主要工作是支持主线程或非守护线程。守护线程不会阻塞主线程的执行,甚至在它完成了它的执行后也会继续运行。

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

什么是多进程?

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

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

QQ截图20231205142331

上图展示了一个多进程环境,其中一个主进程创建了两个单独的进程,并为它们分配了不同的工作。

多线程实现

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

1、导入库:

import threading
import os

2、计算平方的函数:

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

def calculate_squares(numbers):
for num in numbers:
square = num * num
print(
f"Square of the number {num} is {square} | Thread Name {threading.current_thread().name} | PID of the process {os.getpid()}"
)

3、主功能:

我们有一个数字列表,我们将把这个列表平均分成两部分,分别命名为fisrt_half和second_half。现在我们将为这些列表分配两个单独的线程t1和t2。

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()

输出:

Square of the number 1 is 1 | Thread Name t1 | PID of the process 345
Square of the number 2 is 4 | Thread Name t1 | PID of the process 345
Square of the number 5 is 25 | Thread Name t2 | PID of the process 345
Square of the number 3 is 9 | Thread Name t1 | PID of the process 345
Square of the number 6 is 36 | Thread Name t2 | PID of the process 345
Square of the number 4 is 16 | Thread Name t1 | PID of the process 345
Square of the number 7 is 49 | Thread Name t2 | PID of the process 345
Square of the number 8 is 64 | Thread Name t2 | PID of the process 345


Note: All the threads created above are non-daemon threads. To create a daemon thread, you need to write t1.setDaemon(True) to make the thread t1 a daemon thread.

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

你也可以观察到,输出并不是按顺序生成的。在第一行,你会看到由thread1生成的输出,然后在第三行,由thread2生成的输出,然后再由thread1在第四行生成的输出。这清楚地表明,这些线程是并发地工作的。

并发并不意味着这两个线程是并行执行的,因为一次只有一个线程被执行。它并没有减少执行时间。它花费的时间与顺序执行相同。CPU开始执行一个线程,但在中途离开它,转移到另一个线程,然后在一段时间后,回到主线程,并从它上次离开的地方开始执行。

多进程实现

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

我们将遵循相同的例子,但是我们不是创建两个单独的线程,而是创建两个独立的进程,并讨论观察结果。

1、导入库

from multiprocessing import Process
import os

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

2、计算平方的函数:

该功能将保持不变。我们刚刚删除了线程信息的打印语句。

def calculate_squares(numbers):
for num in numbers:
square = num * num
print(
f"Square of the number {num} is {square} | PID of the process {os.getpid()}"
)

3、主功能:

主要功能有一些修改。我们刚刚创建了一个单独的进程而不是线程。

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()

输出:

Square of the number 1 is 1 | PID of the process 1125
Square of the number 2 is 4 | PID of the process 1125
Square of the number 3 is 9 | PID of the process 1125
Square of the number 4 is 16 | PID of the process 1125
Square of the number 5 is 25 | PID of the process 1126
Square of the number 6 is 36 | PID of the process 1126
Square of the number 7 is 49 | PID of the process 1126
Square of the number 8 is 64 | PID of the process 1126

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

计算有无多进程的运行时间

为了检查我们是否得到了真正的并行性,我们将计算算法在有无多进程的情况下的运行时间。

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

from multiprocessing import Process
import os
import time
import random

def calculate_squares(numbers):
for num in numbers:
square = num * num

if __name__ == "__main__":
numbers = [
random.randrange(1, 50, 1) for i in range(10000000)
] # Creating a random list of integers having size 10^7.
half = len(numbers) // 2
first_half = numbers[:half]
second_half = numbers[half:]

# ----------------- Creating Single Process Environment ------------------------#

start_time = time.time() # Start time without multiprocessing

p1 = Process(
target=calculate_squares, args=(numbers,)
) # Single process P1 is executing all list
p1.start()
p1.join()

end_time = time.time() # End time without multiprocessing
print(f"Execution Time Without Multiprocessing: {(end_time-start_time)*10**3}ms")

# ----------------- Creating Multi Process Environment ------------------------#

start_time = time.time() # Start time with multiprocessing

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() # End time with multiprocessing
print(f"Execution Time With Multiprocessing: {(end_time-start_time)*10**3}ms")

输出:

Execution Time Without Multiprocessing: 619.8039054870605ms
Execution Time With Multiprocessing: 321.70287895202637ms

你可以观察到,使用多进程的时间几乎是不使用多进程的一半。这表明这两个进程是同时执行的,并表现出真正的并行性。

文章来源:https://www.kdnuggets.com/introduction-to-multithreading-and-multiprocessing-in-python
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消