Python 中的并发性 - 多处理

在本章中,我们将更多地关注多处理和多线程之间的比较。

多处理

它是在单个计算机系统中使用两个或多个 CPU 单元。这是通过充分利用计算机系统中可用的全部 CPU 内核来充分发挥硬件潜力的最佳方法。

多线程

它是 CPU 通过同时执行多个线程来管理操作系统使用的能力。多线程的主要思想是通过将进程划分为多个线程来实现并行性。

下表显示了它们之间的一些重要区别−

多处理 多道编程
多处理是指多个 CPU 同时处理多个进程。 多道编程将多个程序同时保存在主内存中,并利用单个 CPU 同时执行它们。
它利用多个 CPU。 它利用单个 CPU。
它允许并行处理。 发生上下文切换。
处理作业所需的时间更少。 处理作业所需的时间更多。
它有助于提高效率计算机系统设备的利用率。 效率低于多处理。
通常更昂贵。 这样的系统更便宜。

消除全局解释器锁 (GIL) 的影响

在使用并发应用程序时,Python 中存在一个称为 GIL(全局解释器锁) 的限制。GIL 绝不允许我们使用多个 CPU 核心,因此我们可以说 Python 中没有真正的线程。GIL 是互斥锁,它使线程安全。换句话说,我们可以说 GIL 阻止多个线程并行执行 Python 代码。锁一次只能被一个线程持有,如果我们想执行一个线程,那么它必须先获取锁。

通过使用多处理,我们可以有效地绕过 GIL 造成的限制 −

  • 通过使用多处理,我们利用了多个进程的能力,因此我们利用了 GIL 的多个实例。

  • 因此,我们的程序中一次执行一个线程的字节码没有任何限制。

在 Python 中启动进程

在多处理模块 − 中,可以使用以下三种方法在 Python 中启动进程

  • Fork
  • Spawn
  • Forkserver

使用 Fork 创建进程

Fork命令是 UNIX 中的标准命令。它用于创建称为子进程的新进程。此子进程与称为父进程的进程同时运行。这些子进程也与其父进程相同,并继承父进程可用的所有资源。使用 Fork 创建进程时使用以下系统调用 −

  • fork() − 它是一个通常在内核中实现的系统调用。它用于创建进程的副本。p>

  • getpid() − 此系统调用返回调用进程的进程 ID (PID)。

示例

以下 Python 脚本示例将帮助您了解如何创建新的子进程并获取子进程和父进程的 PID −

import os

def child():
   n = os.fork()
   
   if n > 0:
      print("PID of Parent process is : ", os.getpid())

   else:
      print("PID of Child process is : ", os.getpid())
child()

输出

PID of Parent process is : 25989
PID of Child process is : 25990

使用 Spawn 创建进程

Spawn 表示启动新进程。因此,生成进程表示由父进程创建新进程。父进程继续异步执行或等待子进程结束执行。请按照以下步骤生成进程 −

  • 导入多处理模块。

  • 创建对象进程。

  • 通过调用 start() 方法启动进程活动。

  • 等待进程完成其工作并通过调用 join() 方法退出。

示例

以下 Python 脚本示例有助于生成三个进程

import multiprocessing

def spawn_process(i):
   print ('This is process: %s' %i)
   return

if __name__ == '__main__':
   Process_jobs = []
   for i in range(3):
   p = multiprocessing.Process(target = spawn_process, args = (i,))
      Process_jobs.append(p)
   p.start()
   p.join()

输出

This is process: 0
This is process: 1
This is process: 2

使用 Forkserver 创建进程

Forkserver 机制仅适用于支持通过 Unix 管道传递文件描述符的选定 UNIX 平台。请考虑以下几点以了解 Forkserver 机制的工作原理 −

  • 使用 Forkserver 机制实例化服务器以启动新进程。

  • 然后,服务器接收命令并处理所有创建新进程的请求。

  • 为了创建新进程,我们的 Python 程序将向 Forkserver 发送请求,它将为我们创建一个进程。

  • 最后,我们可以在我们的程序中使用这个新创建的进程。

Python 中的守护进程

Python multiprocessing 模块允许我们通过其守护进程选项拥有守护进程。守护进程或在后台运行的进程遵循与守护线程类似的概念。要在后台执行进程,我们需要将守护进程标志设置为 true。只要主进程正在执行,守护进程就会继续运行,并在完成执行或主程序被终止时终止。

示例

在这里,我们使用与守护线程中相同的示例。唯一的区别是模块从 多线程 更改为 多处理 并将守护进程标志设置为 true。但是,输出会发生变化,如下所示 −

import multiprocessing
import time

def nondaemonProcess():
   print("starting my Process")
   time.sleep(8)
   print("ending my Process")
def daemonProcess():
   while True:
   print("Hello")
   time.sleep(2)
if __name__ == '__main__':
   nondaemonProcess = multiprocessing.Process(target = nondaemonProcess)
   daemonProcess = multiprocessing.Process(target = daemonProcess)
   daemonProcess.daemon = True
   nondaemonProcess.daemon = False
   daemonProcess.start()
   nondaemonProcess.start()

输出

starting my Process
ending my Process

与守护线程生成的输出相比,输出有所不同,因为守护进程没有输出。因此,守护进程在主程序结束后自动结束,以避免正在运行的进程持续存在。

在 Python 中终止进程

我们可以使用 terminate() 方法立即终止进程。我们将使用此方法在完成执行之前立即终止使用函数创建的子进程。

示例

import multiprocessing
import time
def Child_process():
   print ('Starting function')
   time.sleep(5)
   print ('Finished function')
P = multiprocessing.Process(target = Child_process)
P.start()
print("My Process has terminated, terminating main thread")
print("Terminating Child Process")
P.terminate()
print("Child Process successfully terminated")

输出

My Process has terminated, terminating main thread
Terminating Child Process
Child Process successfully terminated

输出显示,程序在执行由 Child_process() 函数创建的子进程之前终止。这意味着子进程已成功终止。

在 Python 中识别当前进程

操作系统中的每个进程都具有称为 PID 的进程标识。在 Python 中,我们可以借助以下命令找出当前进程的 PID −

import multiprocessing
print(multiprocessing.current_process().pid)

示例

以下 Python 脚本示例有助于找出主进程的 PID 以及子进程的 PID −

import multiprocessing
import time
def Child_process():
   print("PID of Child Process is: {}".format(multiprocessing.current_process().pid))
print("PID of Main process is: {}".format(multiprocessing.current_process().pid))
P = multiprocessing.Process(target=Child_process)
P.start()
P.join()

输出

PID of Main process is: 9401
PID of Child Process is: 9402

在子类中使用进程

我们可以通过对 threading.Thread 类进行子类化来创建线程。此外,我们还可以通过对 multiprocessing.Process 类进行子类化来创建进程。对于在子类中使用进程,我们需要考虑以下几点 −

  • 我们需要定义 Process 类的新子类。

  • 我们需要重写 _init_(self [,args] ) 类。

  • 我们需要重写 run(self [,args] ) 方法来实现 Process

  • 我们需要通过调用start() 方法来启动进程。

示例

import multiprocessing
class MyProcess(multiprocessing.Process):
   def run(self):
   print ('called run method in process: %s' %self.name)
   return
if __name__ == '__main__':
   jobs = []
   for i in range(5):
   P = MyProcess()
   jobs.append(P)
   P.start()
   P.join()

输出

called run method in process: MyProcess-1
called run method in process: MyProcess-2
called run method in process: MyProcess-3
called run method in process: MyProcess-4
called run method in process: MyProcess-5

Python 多处理模块 – Pool 类

如果我们在 Python 应用程序中讨论简单的并行处理任务,那么多处理模块会为我们提供 Pool 类。Pool 类的以下方法可用于在主程序中启动子进程数

apply() 方法

此方法类似于 .ThreadPoolExecutor.submit() 方法。它会阻塞,直到结果准备好为止。

apply_async() 方法

当我们需要并行执行任务时,我们需要使用 apply_async() 方法将任务提交到池中。这是一个异步操作,在执行完所有子进程之前不会锁定主线程。

map() 方法

apply() 方法一样,它也会阻塞,直到结果准备好。它相当于内置的 map() 函数,将可迭代数据拆分为多个块,并作为单独的任务提交给进程池。

map_async() 方法

它是 map() 方法的变体,就像 apply_async()apply() 方法的变体一样。它返回一个结果对象。当结果准备好时,将对其应用一个可调用函数。可调用函数必须立即完成;否则,处理结果的线程将被阻塞。

示例

以下示例将帮助您实现用于并行执行的进程池。通过 multiprocessing.Pool 方法应用 square() 函数,执行了数字平方的简单计算。然后使用 pool.map() 提交 5,因为输入是从 0 到 4 的整数列表。结果将存储在 p_outputs 中并打印出来。

def square(n):
   result = n*n
   return result
if __name__ == '__main__':
   inputs = list(range(5))
   p = multiprocessing.Pool(processes = 4)
   p_outputs = pool.map(function_square, inputs)
   p.close()
   p.join()
   print ('Pool :', p_outputs)

输出

Pool : [0, 1, 4, 9, 16]