第 19 章:并发¶
支持并发越来越重要。过去,主流的并发编程通常意味着确保与相对缓慢的网络、磁盘、数据库和其他 I/O 资源交互的代码不会过度减慢速度。利用并行性通常只在诸如在超级计算机上运行的应用程序的科学计算等领域中看到。
但现在有新的因素在起作用。半导体行业继续发奋努力,以维持摩尔定律关于芯片密度呈指数增长的说法。芯片设计师过去将这种红利用于加速单个 CPU。但由于各种原因,这种旧方法不再有效。因此,现在芯片设计师正在用更多 CPU 和硬件线程来填充芯片。加速执行意味着利用硬件的并行性。现在,我们作为软件开发人员的工作就是完成这项工作。
Java 平台可以在这里提供帮助。Java 平台可以说是当今运行并发代码最强大的环境,并且可以从 Jython 中轻松使用此功能。问题仍然是编写并发代码并不容易。这种困难尤其体现在基于线程的并发模型方面,这是当今硬件原生暴露的模型。
这意味着我们必须关注线程安全,这是由于存在线程之间共享的可变对象而产生的问题。(在函数式编程中,可变状态可能是可以避免的,但在除最简单的 Python 代码之外的任何代码中,它都难以避免。)如果你试图通过同步来解决并发问题,你会遇到其他问题。除了潜在的性能下降之外,还有死锁和活锁的可能性。
注意
JVM 的实现,如 HotSpot,通常可以避免同步的开销。我们将在本章后面讨论这种情况发生的必要条件。
鉴于所有这些问题,有人认为线程太难做对了。我们并不认同这种观点。在 JVM 上已经编写了有用的并发系统,其中包括用 Jython 编写的应用程序。编写此类代码的关键成功因素包括
- 保持并发简单。
- 使用任务,这些任务可以映射到线程池。
- 尽可能使用不可变对象。
- 避免不必要的可变对象共享。
- 尽量减少可变对象的共享。队列和相关对象(如同步屏障)提供了一种结构化的机制,用于在线程之间传递对象。这可以实现一种设计,其中对象在状态改变时仅对一个线程可见。
- 防御性编码。使取消或中断任务成为可能。使用超时。
Java 或 Python API?¶
在编写并发代码时,您需要考虑的一个问题是,在多大程度上使您的实现依赖于 Java 平台。以下是我们的建议
- 如果您要移植使用并发的现有 Python 代码库,则可以使用标准 Python
threading
模块。此类代码仍然可以与 Java 交互,因为 Jython 线程始终映射到 Java 线程。(如果您来自 Java,您会认出这个 API,因为它基本上是基于 Java 的。)- Jython 使用 Java 的
ConcurrentHashMap
实现dict
和set
。这意味着您可以直接使用这些标准 Python 类型,并仍然获得高性能并发。(它们也像 CPython 中一样是原子的,正如我们将要描述的。)- 您还可以使用
java.util.concurrent
中的任何集合。因此,如果它适合您的应用程序需求,您可能需要考虑使用诸如CopyOnWriteArrayList
和ConcurrentSkipListMap
(Java 6 中的新增功能)之类的集合。Google Collections 库 是另一个与 Jython 配合良好的不错的选择。- 使用 Java 中的更高级别的原语,而不是创建您自己的原语。这对于针对线程池运行和管理任务的执行器服务尤其如此。例如,避免使用
threading.Timer
,因为您可以使用定时执行服务来代替它。但仍然使用threading.Condition
和threading.Lock
。特别是,这些构造已被优化以在 with 语句的上下文中工作,正如我们将要讨论的。
实际上,使用 Java 对更高级别原语的支持不应该对代码的可移植性产生太大影响。特别是使用任务往往会使所有这些都很好地隔离。并且诸如线程限制和安全发布之类的线程安全注意事项保持不变。
最后,请记住,您始终可以混合搭配。
使用线程¶
创建线程很容易,也许太容易了。此示例并发地下载网页
.. literalinclude:: src/chapter19/test_thread_creation.py
小心不要无意中调用该函数;target
获取对函数对象的引用(通常是普通函数的名称)。调用该函数会创建一个有趣的错误,您的目标函数现在运行,因此一开始一切看起来都很好。但是没有发生并发,因为函数调用实际上是由调用线程运行的,而不是这个新线程。
目标函数可以是普通函数,也可以是可调用对象(实现 __call__
)。后一种情况可能使您难以看出目标是函数对象!
要等待线程完成,请在其上调用 join
。这使得能够使用并发结果。唯一的问题是如何获取结果。正如我们将看到的,在 Jython 中将结果发布到变量是安全的,但这也不是最好的方法。
线程局部变量¶
threading.local
类允许每个线程在共享环境中拥有自己的一些对象的实例。它的用法非常简单。只需创建一个 threading.local
实例,或其子类,并将其分配给一个变量或其他名称。此变量可以是全局的,也可以是某个其他命名空间的一部分。到目前为止,这就像使用 Python 中的任何其他对象一样。
然后线程可以共享该变量,但有一个变化:每个线程将看到该对象的不同的、特定于线程的版本。此对象可以添加任意属性,每个属性对其他线程不可见。
其他选项包括子类化 threading.local
。像往常一样,这允许您定义默认值并指定更细致的属性模型。但一个独特且可能很有用的方面是,在 __slots__
中指定的任何属性都将在线程之间共享。
但是,在使用线程局部变量时,存在一个大问题。通常它们没有意义,因为线程不是正确的范围。对象或函数是,尤其是通过闭包。如果您使用线程局部变量,则隐式地采用了一种模型,其中线程正在划分工作。但随后您将给定的工作绑定到一个线程。这使得使用线程池成为问题,因为您必须清理线程后的工作。
话虽如此,线程局部变量在某些情况下可能很有用。一种常见的情况是您的代码被您没有编写的组件调用。您可能需要访问线程局部单例。当然,如果您使用的是架构强制使用线程局部变量的代码,那么您就必须使用它。
但通常这是不必要的。您的代码可能不同,但 Python 为您提供了很好的工具来避免远程操作。您可以使用闭包、装饰器,甚至有时选择性地修补模块。利用 Python 是一种动态语言的事实,它对元编程有很强的支持。请记住,Jython 实现使这些技术在处理即使是顽固的 Java 代码时也能使用。
最后,线程局部变量是一个有趣的旁注。它们在面向任务的模型中效果不佳,因为您不希望将上下文与将被分配到任意任务的工作线程相关联。如果没有足够的注意,这会导致混乱。
没有全局解释器锁¶
Jython 缺少全局解释器锁 (GIL),这是 CPython 的一个实现细节。对于 CPython,GIL 意味着一次只能运行一个线程的 Python 代码。此限制也适用于大部分支持的运行时以及未释放 GIL 的扩展模块。(不幸的是,到目前为止,在 CPython 中删除 GIL 的开发工作只会导致 Python 执行速度明显下降。)
GIL 对 CPython 编程的影响是,线程不像在 Jython 中那样有用。并发性只会出现在与 I/O 交互以及由扩展模块对 CPython 运行时之外管理的数据结构执行计算的场景中。相反,开发人员通常会使用面向过程的模型来规避 GIL 的限制性。
同样,Jython 没有 GIL 的束缚。这是因为所有 Python 线程都映射到 Java 线程并使用标准 Java 垃圾回收支持(CPython 中 GIL 的主要原因是引用计数 GC 系统)。这里的重要影响是,您可以使用线程来执行用 Python 编写的计算密集型任务。
模块导入锁¶
但是,Python 确实定义了一个模块导入锁,它由 Jython 实现。每当导入任何名称时,都会获取此锁。无论导入是通过 import 语句、等效的 __import__
内置函数还是相关代码进行的,都是如此。重要的是要注意,即使已经导入了相应的模块,模块导入锁仍然会被获取,即使只是短暂地获取。
所以不要在热循环中编写这样的代码,尤其是在线程代码中
def slow_things_way_down():
from foo import bar, baz
...
延迟导入仍然有意义。这种延迟可以减少应用程序的启动时间。请记住,由于此锁,执行此类导入的线程将被迫单线程运行。因此,您的代码可能在后台线程中执行延迟导入是有意义的
.. literalinclude:: src/chapter19/background_import.py
因此,如您所见,您需要至少执行两次给定模块的导入;一个在后台线程中;另一个在实际使用模块命名空间的地方。
以下是我们需要模块导入锁的原因。在第一次导入时,导入过程会运行模块的(隐式)顶层函数。即使许多模块通常是声明性的,但在 Python 中,所有定义都在运行时完成。此类定义可能包括进一步的导入(递归导入)。顶层函数当然可以执行更复杂的任务。模块导入锁简化了此设置,使其安全发布。我们将在本章后面进一步讨论这个概念。
请注意,在当前实现中,模块导入锁对于整个 Jython 运行时是全局的。这在将来可能会改变。
使用任务¶
通常最好避免直接管理线程的生命周期。相反,任务模型通常提供更好的抽象。
任务描述要执行的异步计算。虽然还有其他选择,但您要 submit
执行的对象应该实现 Java 的 Callable
接口(一个没有参数的 call
方法),因为这最适合与 Python 方法或函数一起使用。任务会经历创建、提交(到执行器)、启动和完成的状态。任务也可以被取消或中断。
执行器使用一组线程运行任务。这可能是一个线程、一个线程池,或者尽可能多的线程来并发运行所有当前提交的任务。具体选择构成执行器策略。但通常您希望使用线程池来控制并发程度。
期货允许代码仅在需要时访问任务中计算的结果——或异常(如果抛出)。在此之前,使用代码可以与该任务并发运行。如果它还没有准备好,就会引入等待依赖关系。
我们将通过使用下载网页的示例来了解如何使用此功能。我们将对其进行包装,使其易于使用,并跟踪下载状态以及任何计时信息
.. literalinclude:: src/chapter19/downloader.py
在 Jython 中,任何其他任务都可以以这种方式完成,无论是数据库查询还是用 Python 编写的计算密集型任务。它只需要支持 Callable
接口。
接下来,我们需要创建期货。期货完成后,要么返回结果,要么将异常抛出到调用者。此异常将是以下之一
- InterruptedException
- ExecutionException。您的代码可以使用
cause
属性检索底层异常。
(将异常推送到异步调用者类似于协程在调用 send
时工作的方式。)
现在我们有了将多个网页的下载多路复用到线程池所需的一切
.. literalinclude:: src/chapter19/test_futures.py
直到返回的期货上的 get
方法,调用者与该任务并发运行。然后,get
调用引入了对任务完成的等待依赖关系。(因此,这类似于在支持线程上调用 join
。)
关闭线程池应该像在池上调用 shutdown
方法一样简单。但是,您可能需要考虑到此关闭可能发生在代码中的非常规时间。以下是标准 Java 文档中提供的健壮关闭函数 shutdown_and_await_termination
的 Jython 版本
.. literalinclude:: src/chapter19/shutdown.py
CompletionService
接口为使用期货提供了一个很好的抽象。这种情况是,与我们的代码使用 invokeAll
或以其他方式轮询它们一样,不是等待所有期货完成,而是完成服务将在期货完成时将它们推送到同步队列中。然后,此队列可以被在一个或多个线程中运行的消费者使用
.. literalinclude:: src/chapter19/test_completion.py
此设置使自然流程成为可能。虽然可能很想通过完成服务的队列安排所有内容,但存在限制。例如,如果您正在编写可扩展的网络爬虫,您将希望将此工作队列外部化。但是对于简单的管理,它当然就足够了。
线程安全¶
线程安全解决以下问题
- 两个或多个线程的(意外)交互是否会破坏可变对象?对于像列表或字典这样的集合来说,这尤其危险,因为这种破坏可能会导致底层数据结构无法使用,甚至在遍历它时产生无限循环。
- 更新是否会丢失?也许典型的例子是递增计数器。在这种情况下,在检索当前值和使用递增值更新之间,可能会与另一个线程发生数据竞争。
Jython 确保其底层可变集合类型(dict
、list
和 set
)不会被破坏。但更新仍然可能在数据竞争中丢失。
但是,您的代码可能使用的其他 Java 集合对象通常不会提供这种无破坏保证。如果您需要使用 LinkedHashMap
来支持有序字典,那么如果您要共享和修改它,则需要考虑线程安全。
以下是一个简单的测试工具,我们将在示例中使用它。 ThreadSafetyTestCase
是 unittest.TestCase
的子类,添加了一个新方法 assertContended
.. literalinclude:: src/chapter19/threadsafety.py
这种新方法运行目标函数并断言所有线程都正常终止。然后测试代码需要检查任何其他不变式。
例如,我们在 Jython 中使用这个想法来测试对 list
类型的某些操作是原子的。这个想法是应用一系列操作,执行一个操作,然后将其反转。向前一步,后退一步。最终结果应该回到你开始的地方,一个空列表,这就是测试代码断言的内容。
.. literalinclude:: src/chapter19/test_list.py
当然,这些问题对不可变对象根本不适用。常用的对象,如字符串、数字、日期时间、元组和冻结集是不可变的。你也可以创建自己的不可变对象。
解决线程安全问题还有许多其他策略。我们将按以下方式查看它们。
- 同步
- 原子性
- 线程封闭
- 安全发布
同步¶
我们使用同步来控制线程进入与同步资源相对应的代码块。通过这种控制,我们可以防止数据竞争,假设同步协议正确。(这可能是一个很大的假设!)
一个 threading.Lock
确保只有一个线程可以进入。(在 Jython 中,但与 CPython 不同,这样的锁总是可重入的;threading.Lock
和 threading.RLock
之间没有区别。)其他线程必须等待该线程退出锁。这种显式锁是最简单且可能也是最便携的同步方式。
你通常应该通过 with 语句来管理此类锁的进入和退出;如果失败,你必须使用 try-finally 来确保在退出代码块时始终释放锁。
以下是一些使用 with 语句的示例代码。代码分配一个锁,然后在一些任务之间共享它。
.. literalinclude:: src/chapter19/test_lock.py
:pyobject: LockTestCase.test_with_lock
或者,你可以使用 try-finally 来完成此操作。
.. literalinclude:: src/chapter19/test_lock.py
:pyobject: LockTestCase.test_try_finally_lock
但不要这样做。它实际上比 with 语句慢。而且使用 with 语句版本也会导致更惯用的 Python 代码。
另一种可能性是使用 synchronize
模块,该模块是 Jython 特定的。该模块提供了一个 ``make_synchronized`` 装饰器函数,它将 Jython 中的任何可调用对象包装在一个 synchronized
块中。
.. literalinclude:: src/chapter19/test_synchronized.py
在这种情况下,你不需要显式释放任何东西。即使在出现异常的情况下,同步锁也会在退出函数时始终被释放。同样,此版本也比 with 语句形式慢,并且它不使用显式锁。
Jython 的当前运行时(截至 2.5.1)可以通过运行时支持和此语句的编译方式更有效地执行 with 语句形式。原因是大多数 JVM 可以对代码块(编译单元,包括任何内联)执行分析,以避免同步开销,只要满足两个条件。首先,该块包含锁和解锁。其次,该块对于 JVM 执行其分析来说不太长。with 语句的语义使我们能够在使用内置类型(如
threading.Lock
)时相对容易地做到这一点,同时避免了 Java 运行时反射的开销。将来,对新
invokedynamic
字节码的支持应该会消除这些性能差异。
threading
模块提供了可移植性,但它也是极简的。你可能希望使用 Java.util.concurrent
中的同步器,而不是 threading
中的包装版本。特别是,如果你想在锁上等待一个超时,则需要这种方法。你可能还想使用像 Collections.synchronizedMap
这样的工厂,在适用时,以确保底层的 Java 对象具有所需的同步。
死锁¶
但是要谨慎使用同步。这段代码最终总会发生死锁
.. literalinclude:: src/chapter19/deadlock.py
死锁是由任何长度的等待依赖循环造成的。例如,Alice 正在等待 Bob,但 Bob 正在等待 Alice。如果没有超时或其他策略改变——Alice 只是厌倦了等待 Bob!——这个死锁将无法打破。
避免死锁可以通过永远不获取锁来实现,这样就不会产生这样的循环。如果我们重写示例,使锁按相同顺序获取(Bob 总是允许 Alice 优先),就不会发生死锁。但是,这种排序并不总是那么容易做到。通常,更稳健的策略是允许超时。
其他同步对象¶
Queue
模块实现了先进先出的同步队列。(同步队列也称为阻塞队列,这就是它们在 java.util.concurrent
中的描述方式。)这样的队列代表了一种线程安全的方式,可以将对象从一个或多个生产线程发送到一个或多个消费线程。
通常,您将定义一个 poision 对象来关闭队列。这将允许任何正在消费但正在等待的线程立即关闭。或者直接使用 Java 对执行器的支持来获得现成的解决方案。
如果您需要实现其他策略,例如后进先出或基于优先级,您可以根据需要使用 java.util.concurrent
中的可比较同步队列。(请注意,这些已经在 Python 2.6 中实现,因此在 Jython 2.6 最终发布时将提供。)
Condition
对象允许一个线程 notify
另一个正在等待条件唤醒的线程;notifyAll
用于唤醒所有此类线程。与 Queue
一样,这可能是实际使用中最通用的同步对象。
Condition
对象始终与 Lock
相关联。您的代码需要通过获取相应的锁来括起等待和通知条件,然后最后(像往常一样!)释放它。像往常一样,这在 with 语句的上下文中最容易完成。
例如,以下是如何在 Jython 的标准库中实际实现 Queue
(只是在这里修改为使用 with 语句)。我们不能使用标准的 Java 阻塞队列,因为能够在队列中没有更多工作要执行时加入队列的要求需要第三个条件变量
.. literalinclude:: src/chapter19/Queue.py
还有其他同步机制,包括交换器、屏障、闩锁等。您可以使用信号量来描述多个线程可以进入的场景。或者使用设置为区分读写操作的锁。Java 平台有很多可能性。根据我们的经验,Jython 应该能够与任何一个平台一起工作。
原子操作¶
原子操作本质上是线程安全的。不会发生数据竞争和对象损坏。其他线程也不可能看到不一致的视图。
因此,原子操作比同步更容易使用。此外,原子操作通常会使用 CPU 中的底层支持,例如 compare-and-swap
指令。或者它们也可能使用锁定。重要的是要知道锁不是直接可见的。此外,如果使用同步,则无法扩展同步的范围。特别是,回调和迭代不可行。
Python 保证某些操作的原子性,尽管充其量只是非正式记录的。Fredrik Lundh 关于“Python 中的线程同步方法”的文章总结了邮件列表讨论和 CPython 实现的状态。引用他的文章,以下是对 Python 代码的原子操作
- 读取或替换单个实例属性
- 读取或替换单个全局变量
- 从列表中获取项目
- 就地修改列表(例如,使用 append 添加项目)
- 从字典中获取项目
- 就地修改字典(例如添加项目或调用 clear 方法)
虽然没有明确说明,但这同样适用于内置 set
类型上的等效操作。
对于 CPython,这种原子性源于其全局解释器锁 (GIL)、Python 字节码虚拟机执行循环以及 dict
和 list
等类型在 C 中的原生实现且不释放 GIL 的事实。
尽管这在某种程度上是意外出现的,但它对开发人员来说是一个有用的简化。这也是现有 Python 代码所期望的。因此,我们在 Jython 中实现了这一点。
特别是,因为 dict
是一个 ConcurrentHashMap
,我们还公开了以下方法来原子地更新字典
* ``setifabsent``
* ``update``
需要注意的是,即使在 ConcurrentHashMap
上,迭代也不是原子的。
原子操作很有用,但它们也很有限。通常,您仍然需要使用同步来防止数据竞争。并且必须小心地进行此操作,以避免死锁和饥饿。
线程封闭¶
线程封闭通常是解决使用可变对象时遇到的大多数问题的最佳解决方案。在实践中,您可能不需要共享代码中使用的很大一部分可变对象。简单地说,如果您不共享,那么线程安全问题就会消失。
并非所有问题都可以简化为使用线程封闭。您的系统中可能存在一些共享对象,但在实践中大多数都可以消除。而且通常共享状态是别人的问题。
- 中间对象不需要共享。例如,如果您正在构建一个仅由局部变量指向的缓冲区,则不需要同步。只要您没有试图保留这些中间对象以避免分配开销,这是一个很容易遵循的处方。不要那样做。
- 生产者-消费者。在一个线程中构造一个对象,然后将其传递给另一个线程。您只需要使用适当的同步器对象,例如
Queue
。- 应用程序容器。典型的数据库驱动 Web 应用程序构成了经典案例。例如,如果您使用的是 modjy,那么数据库连接池和线程池是 servlet 容器的责任。而且它们是不可直接观察的。(但不要做跨线程共享数据库连接之类的事情。)缓存和数据库就是您将看到共享状态的地方。
- Actor。Actor 模型是另一个很好的例子。向 Actor(实际上是一个独立的线程)发送和接收消息,并让它代表您操作它拥有的任何对象。实际上,这将问题简化为共享一个可变对象,即消息队列。然后,消息队列可以确保任何访问都得到适当的序列化,因此不存在线程安全问题。
不幸的是,线程封闭在 Jython 中并非没有问题。例如,如果您使用 StringIO
,则必须支付此类使用 list
的成本,而 list
是同步的。虽然可以进一步优化 Jython 对 Python 标准库的实现,但如果代码段足够热,您可能需要考虑用 Java 重写它,以确保没有额外的同步开销。
最后,线程封闭在 Python 中并不完美,因为有可能对帧对象进行内省。这意味着您的代码可以查看其他线程中的局部变量,以及它们指向的对象。但这实际上更多地是关于 Jython 在 JVM 上运行时的可优化性问题。如果您不利用这个漏洞,它不会导致线程安全问题。我们将在关于 Python 内存模型的部分中进一步讨论这一点。
Python 内存模型¶
在 Python 中推理并发比在 Java 中更容易。这是因为内存模型对我们关于程序如何运行的传统推理并不那么令人惊讶。但是,这也意味着 Python 代码牺牲了显著的性能以保持其简单性。
原因如下。为了最大限度地提高 Java 性能,允许 CPU 任意重新排序 Java 代码执行的操作,但要受 _happens-before_ 和 _synchronizes-with_ 关系的约束。(已发布的 Java 内存模型 对这些约束进行了更详细的说明。)
尽管这种重新排序在一个给定线程内不可见,但问题是它对其他线程可见。当然,这种可见性仅适用于对非局部对象的更改;线程封闭仍然适用。
特别是,这意味着在查看两个或多个线程时,您不能依赖 Java 代码的明显顺序。
Python 不同。关于 Python 的基本知识,以及我们在 Jython 中实现的内容是,在 Python 中设置任何属性都是一个 volatile 写入;获取任何属性都是一个 volatile 读取。这是因为 Python 属性存储在字典中,在 Jython 中,这遵循了支持 ConcurrentHashMap
的语义。所以 get
和 set
是 volatile 的。
因此这意味着 Python 代码具有顺序一致性。执行遵循代码中语句的顺序。这里没有惊喜。
这意味着与 Java 相比,安全发布在 Python 中非常简单。安全发布意味着将对象与名称安全地关联。因为这在 Python 中始终是一个内存围栏操作,所以您的代码只需要确保对象本身以线程安全的方式构建;然后通过将适当的变量设置为该对象来立即发布它。
如果您需要创建模块级对象 - 单例 - 那么您应该在模块的顶层脚本中执行此操作,以便模块导入锁生效。
中断¶
长时间运行的线程应该提供一些取消的机会。典型的模式是这样的
class DoSomething(Runnable):
def __init__(self):
cancelled = False
def run(self):
while not self.cancelled:
do_stuff()
请记住,与 Java 不同,Python 变量始终是 volatile 的。使用这样的 cancelled
标志没有问题。
线程中断允许更灵敏的取消。特别是,如果一个线程正在等待大多数同步器,例如条件变量或文件 I/O,此操作将导致等待的方法以 InterruptedException
退出。(不幸的是,除了在某些情况下(例如在底层 Java 锁上使用 lockInterruptibly
)之外,锁获取是不可中断的。)
虽然 Python 的 threading
模块本身不支持中断,但它可以通过标准 Java 线程 API 获得。首先,让我们导入此类。我们将将其重命名为 JThread
,这样它就不会与 Python 的版本冲突
from java.lang import Thread as JThread
正如我们所见,您可以将 Java 线程用作 Python 线程。因此,从逻辑上讲,您应该能够做相反的事情:将 Python 线程用作 Jave 线程。因此,能够进行像 JThread.interrupt(obj)
这样的调用会很好。
顺便说一句,这种公式,而不是obj.interrupt()
,看起来像是类上的静态方法,只要我们将对象作为第一个参数传入。这种调整是 Python 显式 self 的一个很好的用法。
但这里有一个问题。截至最新发布版本(Jython 2.5.1),我们忘记在 Thread
类上包含一个合适的 __tojava__
方法!所以看起来你毕竟不能做这个技巧。
或者你可以?如果你不必等到我们修复这个错误怎么办?您可以探索源代码 - 或者使用 dir
查看该类。一种可能性是使用 Thread
对象上的名义私有 _thread
属性。毕竟 _thread
是底层 Java 线程的属性。是的,这是一个实现细节,但使用它可能没问题。它不太可能改变。
但我们可以做得更好。我们可以猴子补丁 Thread
类,使其具有一个合适的 __tojava__
方法,但前提是它不存在。因此,这种修补很可能适用于未来版本的 Jython,因为在我们甚至考虑更改其实现并删除 _thread
之前,我们将修复此缺少的方法。
以下是我们如何进行猴子补丁,遵循 Guido van Rossum 的方法
.. literalinclude:: src/chapter19/monkeypatch.py
这个 monkeypatch_method
装饰器允许我们在事后向类添加方法。(这就是 Ruby 开发人员所说的打开类。)谨慎使用这种能力。但同样,当你将此类修复保持在最低限度时,你不必太担心,尤其是在它本质上是一个像这样的错误修复时。在我们的例子中,我们将使用一个变体,即 monkeypatch_method_if_not_set
装饰器,以确保我们只在没有被更高版本修复的情况下进行补丁。
将所有内容放在一起,我们有以下代码
.. literalinclude:: src/chapter19/interrupt.py
(它确实依赖于使用 threading.Condition
来获得可以等待的东西。我们将在后面讨论条件变量。)
最后,你可以简单地通过 Future
提供的 cancel
方法访问中断。无需猴子补丁!
结论¶
Jython 可以充分利用底层 Java 平台对并发性的支持。你也可以使用标准的 Python 线程构造,它们在大多数情况下只是包装了相应的 Java 功能。标准的可变 Python 集合类型在 Jython 中已经考虑了并发性而实现。而 Python 的顺序一致性消除了某些潜在的错误。
但并发编程仍然不容易做到,无论是在 Python 还是 Java 中。你应该考虑更高层次的并发原语,例如任务。并且你应该在代码如何共享可变状态方面保持纪律。