正如我们在前一章中所看到的,在处理任何大型企业应用时,我们都会处理大量数据。该数据以同步方式进行处理,结果仅在特定进程的数据处理完成后发送。当单个请求中处理的数据不大时,这种模型是绝对好的。但是考虑这样一种情况,即在生成响应之前需要处理大量数据。然后呢?答案是,应用响应时间慢。
我们需要一个更好的解决方案。这是一种允许我们并行处理数据的解决方案,可以加快应用响应速度。但我们如何做到这一点?这个问题的答案是并发性。。。
可以通过运行以下命令克隆代码示例:
git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python本章中提到的代码示例需要运行 Python 3.6 及更高版本。虚拟环境是保持依赖项与系统隔离的首选选项。
大多数时候,当我们构建相当简单的应用时,我们不需要并发性。简单的顺序编程工作得很好,其中一个步骤在另一个步骤完成后执行。但是,随着应用用例变得越来越复杂,越来越多的任务可以轻松地推到后台以改善应用的用户体验,我们最终将围绕并发的概念展开讨论。
并发本身就是另一种野兽,它使编程任务变得更加复杂。但是,尽管增加了复杂性,并发也带来了许多特性来改善应用的用户体验。
在我们深入探讨为什么我们。。。
我们已经习惯使用的硬件一年比一年强大。今天,即使是智能手机中的 CPU 也有四核或八核配置。这些配置允许并行运行多个进程或线程。不利用并发的能力将是对前面提到的硬件改进的浪费。今天,当我们在智能手机上打开应用时,大多数应用都有两个或两个以上的线程在运行,尽管我们大多数时候都不知道这一点。
让我们考虑一个简单的例子,在我们的设备上打开一个照片库应用。我们一打开照片库,申请过程就开始了。此过程负责加载应用的 GUI。GUI 在主线程中运行,允许我们与应用交互。现在,这个应用还产生了另一个后台线程,负责遍历操作系统的文件系统并加载照片的缩略图。从文件系统加载缩略图可能是一项乏味的任务,并且可能需要一些时间,具体取决于需要加载的缩略图数量。
尽管我们注意到缩略图加载速度很慢,但在整个过程中,我们的应用 GUI 保持响应,我们可以与之交互,查看进度,等等。所有这些都是通过使用并发编程实现的。
想象一下,如果这里没有使用并发。应用将在主线程中加载缩略图。这将导致 GUI 在主线程完成加载缩略图之前没有响应。这不仅会非常不直观,还会造成糟糕的用户体验,我们通过并发编程避免了这种情况。
现在我们对并发编程如何被证明是非常有用的有了一个大致的概念,让我们看看它如何帮助我们设计和开发企业应用,以及它可以实现什么。
企业应用很大,通常处理许多用户发起的操作,如数据检索、更新等。现在,让我们为我们的 BugZOT 应用提供一个简短的场景,其中用户可以连同他们的 bug 报告一起提交图形附件。这实际上是一个非常常见的过程,当提交可能影响应用 UI 或在 UI 上显示错误的错误时。现在,每个用户都可以提交一张图像,这张图像的质量可能会有所不同,因此它们的大小可能会有所不同。这可能涉及尺寸非常小的图像以及尺寸非常大和分辨率很高的图像。作为一名应用开发人员,您可能知道以 100%的质量存储图像至少可以。。。
Python 提供了许多实现并行性或并发性的方法。所有这些方法都有各自的优点和缺点,并且在如何实现方面存在根本性的差异,需要在记住用例的情况下选择何时使用哪种方法。
Python 提供的实现并发性的方法之一是在线程级别执行的,它允许应用启动多个线程,每个线程执行一个作业。这些线程提供了易于使用的并发机制,并在单个 Python 解释器进程内执行,因此是轻量级的。
实现并行性的另一种机制是使用多个进程代替多个线程。使用这种方法,每个进程在其各自的 Python 解释器进程内执行单独的任务。这种方法为多线程 Python 程序在存在全局解释器锁(GIL的情况下可能面临的问题提供了一些解决方法,我们将在本章后面的章节中讨论这些问题,但也可能会增加管理多个进程的额外开销,并增加内存使用。
首先,让我们看看如何使用线程实现并发性,并讨论它们打包的好处和缺点。
在大多数现代处理器系统中,使用多线程是司空见惯的。由于 CPU 具有多个核心和超线程等技术,允许单个核心同时运行多个线程,应用开发人员不会浪费任何机会利用这些技术提供的优势。
Python 作为一种编程语言,通过使用线程模块支持多线程的实现,该模块允许开发人员利用应用中的线程级并行性。
以下示例展示了如何使用 Python 中的线程模块构建简单程序:
# simple_multithreading.pyimport threadingclass SimpleThread(threading.Thread): ...正如我们在上一节中所探讨的,虽然线程可以很容易地在 Python 中实现,但它们确实有自己的局限性,在尝试编写针对生产用例的应用时,需要注意这些局限性。如果在应用开发时不考虑这些问题,它们将产生难以调试的行为,而并发程序正是因为这些行为而出名的。
那么,让我们来看看如何解决上一节讨论的问题。如果我们认真思考,我们可以将问题归类为多线程同步的问题。应用的最佳行为是以这样的方式同步对文件的写入,即在任何给定时间点只有一个线程能够写入文件。这将强制任何线程在已经执行的线程之一完成写入之前都不能启动写入操作。
要实现这种同步,我们可以利用锁定的功能。锁提供了实现同步的简单方法。例如,要开始写操作的线程将首先获取锁。如果锁获取成功,线程可以继续执行写操作。现在,如果在这两个线程之间发生上下文切换,而另一个线程即将开始写入操作,那么它将被阻塞,因为锁已经被占用。这将防止线程在已经运行的写入操作之间写入数据。
在 Python 多线程中,我们可以通过使用threading.Lock类来实现锁。该类提供了两种方法,以便于获取和释放锁。当线程希望在执行操作之前获取锁时,acquire()方法由线程调用。一旦获得锁,线程将继续执行操作。一旦线程的操作完成,线程就会调用release()方法来释放锁,以便可能正在等待它的另一个线程可以获取锁。
让我们看看如何使用锁来同步 JSON 到 YAML 转换器示例中的线程操作。以下代码示例演示了锁的使用:
import threading
import json
import yaml
class JSONConverter(threading.Thread):
def __init__(self, json_file, yaml_file, lock):
threading.Thread.__init__(self)
self.json_file = json_file
self.yaml_file = yaml_file
self.lock = lock
def run(self):
print("Starting read for {}".format(self.json_file))
self.json_reader = open(self.json_file, 'r')
self.json = json.load(self.json_reader)
self.json_reader.close()
print("Read completed for {}".format(self.json_file))
print("Writing {} to YAML".format(self.json_file))
self.lock.acquire() # We acquire a lock before writing
self.yaml_writer = open(self.yaml_file, 'a+')
yaml.dump(self.json, self.yaml_writer)
self.yaml_writer.close()
self.lock.release() # Release the lock once our writes are done
print("Conversion completed for {}".format(self.json_file))
files = ['file1.json', 'file2.json', 'file3.json']
write_lock = threading.Lock()
conversion_threads = []
for file in files:
converter = JSONConverter(file, 'converted.yaml', write_lock)
conversion_threads.append(converter)
converter.start()
for cthread in conversion_threads:
cthread.join()
print("Exiting")在本例中,我们首先通过创建threading.Lock类的实例来创建lock变量。然后将该实例传递给所有需要同步的线程。当线程必须执行写操作时,它首先获取锁,然后开始写操作。一旦这些写入完成,线程就会释放锁以供其他线程获取。
If a thread acquires a lock but forgets to release it, the program may get into a state of deadlock since no other thread will be able to proceed. Proper caution should be taken so that the acquired locks are released once the thread finishes its operations, to avoid deadlocks.
threading.Lock类为多线程提供了通用的锁定机制,在该类中,在释放锁之前只能获取一次锁。除此之外,Python 还提供了另一种锁定机制,可能对实现递归操作的程序有用。该锁称为重入锁,使用threading.RLock类实现,可由递归函数使用。该类提供了与 lock 类类似的方法:acquire()和release(),分别获取和释放已获取的锁。唯一的区别是递归函数在调用堆栈中多次调用acquire()时。当同一函数反复调用 acquire 方法时。。。
让我们想象一下,不知何故,我们有一种方法,通过这种方法,我们可以告诉我们的Thread-1等待Thread-2提供一些数据供消费。这正是条件变量允许我们做的。它们允许我们同步依赖于共享资源的两个线程。为了进一步了解这一点,让我们来看看下面的代码示例,它创建两个线程,一个在电子邮件 ID 中提供,另一个线程负责发送电子邮件:
# condition_variable.py
import threading
class EmailQueue(threading.Thread):
def __init__(self, email_queue, max_items, condition_var):
threading.Thread.__init__(self)
self.email_queue = email_queue
self.max_items = max_items
self.condition_var = condition_var
self.email_recipients = []
def add_recipient(self, email):
self.email_recipients.append(email)
def run(self):
while True:
self.condition_var.acquire()
if len(self.email_queue) == self.max_items:
print("E-mail queue is full. Entering wait state...")
self.condition_var.wait()
print("Received consume signal. Populating queue...")
while len(self.email_queue) < self.max_items:
if len(self.email_recipients) == 0:
break
email = self.email_recipients.pop()
self.email_queue.append(email)
self.condition_var.notify()
self.condition_var.release()
class EmailSender(threading.Thread):
def __init__(self, email_queue, condition_var):
threading.Thread.__init__(self)
self.email_queue = email_queue
self.condition_var = condition_var
def run(self):
while True:
self.condition_var.acquire()
if len(self.email_queue) == 0:
print("E-mail queue is empty. Entering wait state...")
self.condition_var.wait()
print("E-mail queue populated. Resuming operations...")
while len(self.email_queue) is not 0:
email = self.email_queue.pop()
print("Sending email to {}".format(email))
self.condition_var.notify()
self.condition_var.release()
queue = []
MAX_QUEUE_SIZE = 100
condition_var = threading.Condition()
email_queue = EmailQueue(queue, MAX_QUEUE_SIZE, condition_var)
email_sender = EmailSender(queue, condition_var)
email_queue.start()
email_sender.start()
email_queue.add_recipient("joe@example.com")在这个代码示例中,我们定义了两个类,即EmailQueue,它扮演生产者的角色,并用需要发送电子邮件的电子邮件地址填充电子邮件队列。然后还有另一个类EmailSender,它扮演消费者的角色,使用电子邮件队列中的电子邮件地址并向他们发送邮件。
现在,在EmailQueue的__init__方法中,我们引入了一个 Python 列表,我们将其用作一个队列作为参数,一个定义列表最多应包含多少项的变量,以及一个条件变量。
接下来,我们有一个方法add_recipient,它在EmailQueue的内部数据结构中附加了一个新的电子邮件 ID,以临时保存电子邮件地址,直到它们被添加到发送队列中。
现在,让我们进入run()方法,实际的魔法发生在这里。首先,我们启动一个无限循环,使线程始终处于运行模式。接下来,我们通过调用条件变量的acquire()方法来获取锁。我们这样做是为了防止线程在意外时间切换上下文时对数据结构造成任何形式的损坏。
获得锁后,我们会检查电子邮件队列是否已满。如果已满,则打印一条消息并调用条件变量的 wait() 方法。对wait()方法的调用将释放条件变量获取的锁,并使线程进入阻塞状态。只有在条件变量上调用 notify() 方法时,此阻塞状态才会结束。现在,当线程通过notify()接收到一个信号时,它继续其操作,首先检查内部队列中是否有一些数据。如果它在内部队列中发现一些数据,那么它会用这些数据填充电子邮件队列,并调用条件变量的 notify() 方法来通知EmailSender消费者线程。现在,让我们看看 AutoT6T.类。
在这里,我们不必逐行讨论,我们将重点放在EmailSender类的run()方法上。因为这个线程需要一直运行,所以我们首先启动一个无限循环来完成这个任务。然后,我们要做的下一件事是,获取共享条件变量的锁。一旦我们获得了锁,我们现在就可以操作共享的email_queue数据结构。因此,我们的消费者要做的第一件事就是检查电子邮件队列是否为空。如果发现队列为空,我们的消费者将调用条件变量的wait()方法,有效地使其释放锁并进入阻塞状态,直到电子邮件队列中有一些数据。这导致控制权转移到负责填充队列的EmailQueue类。
现在,一旦电子邮件队列中有一些电子邮件 ID,消费者将开始发送邮件。一旦它耗尽了队列,它就通过调用条件变量notify方法向EmailSender类发出关于这一点的信号。这将允许EmailSender继续其填充电子邮件队列的操作。
让我们看看当我们试图执行前面的示例程序时会发生什么:
python condition_variable.py
E-mail queue is empty. Entering wait state...
E-mail queue populated. Resuming operations...
Sending email to joe@example.com
E-mail queue is empty. Entering wait state...通过这个例子,我们现在了解了如何在 Python 中使用条件变量来解决生产者-消费者问题。考虑到这些知识,现在让我们来看看在应用中执行多线程时可能会出现的一些问题。
多线程提供了很多好处,但也有一些缺陷。如果不能避免这些陷阱,当应用投入生产时,可能会是一次痛苦的经历。这些陷阱通常会导致意外的行为,这些行为可能只是偶尔发生一次,或者可能发生在特定模块的每次执行中。令人痛苦的是,如果这些问题是由多个线程的执行引起的,那么调试这些问题就非常困难,因为很难预测特定线程何时执行。因此,有必要讨论为什么会出现这些常见陷阱,以及如何在开发阶段避免这些陷阱。
一些常见的原因。。。
在多线程上下文中,争用条件是两个或多个线程试图同时修改共享数据结构,但由于线程的调度和执行方式,共享数据结构的修改方式使其处于不一致状态。
这句话令人困惑吗?不用担心,让我们试着用一个例子来理解它:
考虑我们以前的 JSON 到 YAML 转换器问题的例子。现在,假设在将转换后的 YAML 输出写入文件时没有使用锁。现在考虑这一点:我们有两个线程,名为 EndoT0}和 AuthT1,负责编写普通的 YAML 文件。现在,想象一下writer-1和writer-2两个线程都开始了写入文件的操作,并且按照操作系统计划线程执行的方式,writer-1开始写入文件。现在,当writer-1线程写入文件时,操作系统决定该线程完成其时间配额,并将该线程与writer-2线程交换。现在,这里需要注意的一点是,writer-1线程在交换时没有完成所有数据的写入。现在,writer-2线程开始执行并完成 YAML 文件中的数据写入。writer-2线程完成后,操作系统再次开始执行writer-1线程,该线程再次开始将剩余数据写入 YAML 文件,然后完成。
现在,当我们打开 YAML 文件时,我们看到的是一个包含来自两个 writer 线程的数据的文件,因此使文件处于不一致的状态。诸如writer-1和writer-2线程之间发生的问题称为竞争条件。
竞争条件属于很难调试的问题类别,因为线程的执行顺序取决于机器对机器和操作系统对操作系统。因此,可能发生在一个部署上的问题可能不会发生在另一个部署上。
那么,我们如何避免比赛条件呢?嗯,我们已经有了这个问题的答案,而且我们最近刚刚使用了它们。那么,让我们来看看一些可以防止种族条件的方法:
- 在关键区域使用锁:关键区域是指线程正在修改共享变量的代码区域。为了防止竞争条件在关键区域发生,我们可以使用锁。锁本质上会导致除持有锁的线程之外的所有线程阻塞。需要修改共享资源的所有其他线程只有在当前持有锁的线程释放它时才会执行。可以使用的锁的一些类别是互斥锁,一次只能由一个线程持有;可重入锁,允许递归函数在同一共享资源上使用多个锁;和条件对象,可用于同步生产者-消费者类型环境中的执行。
- 使用线程安全数据结构:防止竞争条件的另一种方法是使用线程安全数据结构。线程安全数据结构将自动管理多个线程对其所做的修改,并将其操作序列化。Python 提供的线程安全共享数据结构之一是队列。当操作涉及多个线程时,可以轻松使用队列。
现在,我们知道了什么是比赛条件,它们是如何发生的,以及如何避免。考虑到这一点,让我们来看看,由于我们防止种族状况发生的方式,可能会出现的其他陷阱之一。
死锁是指两个或多个线程由于相互依赖或资源永远无法释放而永远被阻塞的情况。让我们通过一个简单的例子来了解死锁是如何发生的:
考虑我们前面的 JSON 到 YAML 转换器示例。现在,让我们假设我们在线程中使用了锁,当线程开始写入文件时,它首先对文件使用互斥锁。现在,在线程释放互斥锁之前,其他线程无法执行。
好了,让我们想象一下相同的情况,两个线程, writer-1 和 writer-2 正在尝试写入公共输出文件。现在,当writer-1开始执行时,它首先获取文件的锁并开始其操作。。。
如果有人告诉你,即使你创建了一个多线程程序,一次也只能执行一个线程,你会怎么做?当系统由单个内核组成,一次只能执行一个线程时,这种情况曾经是真实的,CPU 在线程之间频繁切换会产生多个运行线程的假象。
但是这种情况在 Python 的一个实现中也是如此。Python 的原始实现(也称为 CPython)由一个全局互斥体(也称为 GIL)组成,它一次只允许一个线程执行 Python 字节码。这有效地限制了应用一次只能执行一个线程。
在 CPython 中引入 GIL 是因为 CPython 解释器不是线程安全的。GIL 通过交易并发运行多个线程的属性,被证明是解决线程安全问题的有效方法。
GIL 的存在在 Python 社区一直是一个备受争议的话题,人们提出了很多消除 GIL 的建议,但由于各种原因,没有一个建议将其应用到 Python 的生产版本中,这包括对单线程应用的性能影响,打破依赖于 GIL 存在的功能的向后兼容性,等等。
o,GIL 的存在对您的多线程应用意味着什么?实际上,如果您的应用利用多线程来执行 I/O 工作负载,那么您可能不会受到 GIL 造成的性能损失的影响,因为大部分 I/O 发生在 GIL 之外,因此多个线程可以被多路复用。只有当应用使用多个线程来执行 CPU 密集型任务(这些任务需要大量操作特定于应用的数据结构)时,才会感受到 GIL 的影响。由于所有数据结构操作都涉及 Python 字节码的执行,GIL 将通过不允许多个线程并发执行严重限制多线程应用的性能。
那么,对于 GIL 造成的问题,是否有解决办法?答案是肯定的,但是应该采用哪种解决方案完全取决于应用的用例。以下选项有助于避免 GIL:
- **切换 Python 实现:**如果您的应用不一定依赖于底层 Python 实现,并且可以切换到另一个实现,那么有些 Python 实现不附带 GIL。一些没有 GIL 的实现是:Jython 和 IronPython,它们可以完全利用多处理器系统来执行多线程应用。
- **使用多处理:**Python 在构建考虑并发性的程序时有很多选择。我们探讨了多线程,这是实现并发的选项之一,但受到 GIL 的限制。实现并发的另一个选择是使用 Python 的多处理功能,它允许启动多个进程来并行执行任务。由于每个进程都在自己的 Python 解释器实例中运行,因此 GIL 在这里不是问题,它允许充分利用多处理器系统。
了解 GIL 如何影响多线程应用后,现在让我们讨论多处理如何帮助您克服并发限制。
Python 语言提供了一些在应用中实现并发性的简单方法。我们在 Python 线程库中看到了这一点,Python 多处理功能也是如此。
如果您想在程序中借助多处理来构建并发性,那么很容易实现,这都要归功于 Python 多处理库和该库公开的 API。
那么,当我们说我们将通过使用多处理来实现并发时,我们的意思是什么呢。让我们试着回答这个问题。通常,当我们谈论并发时,有两种方法可以帮助我们实现它。其中一种方法是运行单个应用实例并允许它使用多个线程。。。
Python 提供了一种实现多进程程序的简单方法。Python 多处理模块简化了实现,它提供了重要的类,如启动新进程的Process类;Queue类和Pipe类,以方便多个进程之间的通信;等等
以下示例简要介绍了如何使用 Python 的多处理库创建 URL 加载程序,该加载程序作为单独的进程来加载 URL:
# url_loader.py
from multiprocessing import Process
import urllib.request
def load_url(url):
url_handle = urllib.request.urlopen(url)
url_data = url_handle.read()
# The data returned by read() call is in the bytearray format. We need to
# decode the data before we can print it.
html_data = url_data.decode('utf-8')
url_handle.close()
print(html_data)
if __name__ == '__main__':
url = 'http://www.w3c.org'
loader_process = Process(target=load_url, args=(url,))
print("Spawning a new process to load the url")
loader_process.start()
print("Waiting for the spawned process to exit")
loader_process.join()
print("Exiting…")在本例中,我们使用 Python 多处理库创建了一个简单的程序,该程序在后台加载 URL 并将其信息打印到stdout。这里有趣的一点是理解在我们的程序中产生一个新过程是多么容易。那么,让我们来看一看。为了实现多处理,我们首先从 Python 的多处理模块中导入Process类。下一步是创建一个函数,将要加载的 URL 作为参数,然后使用 Python 的urllib模块加载该 URL。加载 URL 后,我们将数据从 URL 打印到stdout。
在 ext 中,我们定义程序开始执行时运行的代码。在这里,我们首先用 url 变量定义了要加载的 URL。下一位是我们通过创建Process类的对象在程序中引入多处理的地方。对于这个对象,我们提供目标参数作为我们想要执行的函数。这类似于我们在使用 Pythonthreading库时逐渐习惯的目标方法。Process构造函数的下一个参数是args参数,它接收调用目标函数时需要传递给目标函数的参数。
为了产生一个新进程,我们调用了Process对象的 start() 方法。这产生了一个新的过程,在这个过程中,我们的目标函数开始执行并发挥它的魔力。我们要做的最后一件事是通过调用Process类的 join() 方法来等待这个派生的进程退出。
这与用 Python 创建多进程应用一样简单。
现在,我们知道如何用 Python 创建多进程应用,但是如何在多个进程之间划分一组特定的任务。嗯,那很容易。下面的代码示例修改了前面示例中的入口点代码,以利用多处理模块中Pool类的功能来实现这一点:
from multiprocessing import Pool
if __name__ == '__main__':
url = ['http://www.w3c.org', 'http://www.microsoft.com', '[http://www.wikipedia.org', '[http://www.packt.com']
with Pool(4) as loader_pool:
loader_pool.map(load_url, url)在本例中,我们使用多处理库中的Pool类创建了一个包含四个进程的池,这些进程将执行我们的代码。然后,我们使用Pool类的map方法,将输入数据映射到单独进程中的执行函数,以实现并发性。
现在,我们的任务中有多个流程。但是,如果我们想让这些过程相互通信呢。例如,在前面的 URL 加载问题中,不是在stdout上打印数据,而是希望流程返回该数据?答案在于使用了管道,它为进程之间的通信提供了双向机制。
以下示例利用管道使 URL 加载器将从 URL 加载的数据发送回父进程:
# url_load_pipe.py
from multiprocessing import Process, Pipe
import urllib.request
def load_url(url, pipe):
url_handle = urllib.request.urlopen(url)
url_data = url_handle.read()
# The data returned by read() call is in the bytearray format. We need to
# decode the data before we can print it.
html_data = url_data.decode('utf-8')
url_handle.close()
pipe.send(html_data)
if __name__ == '__main__':
url = 'http://www.w3c.org'
parent_pipe, child_pipe = Pipe()
loader_process = Process(target=load_url, args=(url, child_pipe))
print("Spawning a new process to load the url")
loader_process.start()
print("Waiting for the spawned process to exit")
html_data = parent_pipe.recv()
print(html_data)
loader_process.join()
print("Exiting…")在本例中,我们使用管道为父进程和子进程提供了双向通信机制,以便它们相互通信。当我们在代码的__main__部分调用pipe构造函数时,构造函数返回一对连接对象。这些连接对象中的每一个都包含一个send()和一个recv()方法,用于促进端部之间的通信。使用send()方法从child_pipe发送的数据可由使用parent_pipe的recv()方法的parent_pipe读取,反之亦然。
If two processes read or write from/to the same end of pipe at the same time, there is the potential for possible data corruption in the pipe. Although, if the processes are using two different ends or two different pipes, this does not become an issue. Only the data that can be pickled can be sent through the pipes. This is one of the limitations of the Python multiprocessing module.
与同步线程的操作一样重要,在多处理上下文中同步操作也很重要。由于多个进程可能正在访问同一共享资源,因此需要序列化它们对共享资源的访问。为了帮助实现这一点,我们这里也支持锁。
以下示例演示了如何在多处理模块的上下文中使用锁,通过获取与 URL 关联的 HTML 并将该 HTML 写入公共本地文件来同步多个进程的操作:
# url_loader_locks.pyfrom multiprocessing import Process, Lockimport urllib.requestdef load_url(url, lock): url_handle = urllib.request.urlopen(url) ...在本章中,我们探讨了如何在 Python 应用中实现并发性,以及它是如何有用的。在本次探索中,我们揭示了 Python 多线程模块的功能,以及如何使用它生成多个线程来分配工作负载。然后,我们进一步了解了如何同步这些线程的操作,并了解了多线程应用中可能出现的各种问题,如果不加以解决的话。本章接着探讨了在一些 Python 实现中存在的全局解释器锁(GIL)所带来的限制,以及它如何影响多线程工作负载。为了探索克服 GIL 限制的可能方法,我们继续了解 Python 的多处理模块的使用,以及它如何通过使用多个进程而不是多个线程来实现并行性,从而帮助我们充分利用多处理器系统的潜力。
- Python 支持构建并发应用的不同方法有哪些?
- 如果获取锁的线程突然终止,那么获取锁会发生什么情况?
- 当应用收到终止信号时,我们如何终止正在执行的线程?
- 我们如何在多个进程之间共享状态?
- 是否有一种方法可以创建一个流程池,然后用于处理任务队列中的传入任务集