当前位置: 首页 > 科技观察

Python并发:线程和锁

时间:2023-03-13 05:17:27 科技观察

概述线程和锁是底层硬件的软件定义形式化,因此包含尽可能简单的并发模型。它构成了构建在其之上的其他并发抽象的基础,因此了解这一点很重要。然而,直接在这些基础上构建可靠、可扩展的系统是困难的或不可能的。虽然大多数语言都支持线程和锁,但是CPython还是使用全局解释器锁来防止线程同时访问共享内存,因为CPython的内存管理不是线程安全的。虽然阻塞操作发生在GIL之外并且可能会提高性能,但线程切换所需的系统调用开销可能会降低性能。这意味着Python中的线程主要用于I/O-bound场景而不是CPU-bound场景。顺便说一句,我提到CPython是因为Python规范的其他部分实现(例如Jython)没有全局解释器锁。然而,这些实现并没有在实践中广泛使用,因为***:没有人愿意支持多个Python实现,除非他们必须这样做;第二:他们不够富有;第三:由于需要原生支持C/C++扩展API,而Python语言定义与C/C++紧耦合。它与其说是一个技术规范,不如说是一个参考实现。Python通过高层模块threading模块和底层模块_thread直接支持线程。有关这些模块如何工作的更多信息,可在线获取源代码。Python入门中典型的单线程“HelloWorld”执行非常简单:多线程模拟没有太大区别:基于我有限的测试次数,上面的脚本是这样运行的:我使用了get_ident()打印“线程标识符”(一个神奇的值,除了在运行时消除不同线程之间的歧义外,没有任何意义)。您可以看到在某些情况下线程标识符是如何不同的,而在其他情况下它们是相同的。相同的线程标识符并不意味着工作仍在同一个线程上,但如果工作不重叠并且不需要不同的线程标识符,Python将重用它。陷阱:时序和一致性如果使用threading.current_thread().getName()交换线程标识符和线程名称,您可能会得到有序的结果,可能是因为每个线程使用相同的函数和源路径,所以每个线程之间的延迟差异线程是微不足道的,仅次于解释器延迟。但是,这并不意味着可以保证按顺序执行;这里有一个来自WikiBooks上“PythonProgramming”的例子,其中每个线程的创建和每个线程的执行都有明显不同的时间:以下结果是相同的示例运行的输出:此日志表明线程创建/执行是交错的.由于添加功能的可变性增加,线程创建和执行之间的时间变得越来越不一致,因此这些结果将变得越来越不可预测。但原则是一样的;使用多线程时不能保证一致的行为。陷阱:访问共享内存当不同的线程访问共享内存时,这会导致不正确的行为。您可以扩展此示例以查看使用多个线程进行计数时的竞争条件:这会在一个示例运行中产生如下输出:此结果随创建的线程数而变化,但您可以看到结果28与预期值100什么一个区别。Counter().count不是线程安全的,在此处演示(如果您的机器与我的机器不同,您可能会得到与28不同的结果)。如果遇到竞争条件,没有足够的日志记录,可能很难找到相关的代码部分。陷阱:死锁当两个代理试图获取同一共享内存区域时,最终会发生死锁。在处理线程和锁的低级抽象时,唯一的解决方案是确保每个代理都有一种方法来正确管理它的锁,或者有一个锁协调的整体规范。例如,用餐哲学问题强调了流程同步的重要性。RosettaCode的diningphilosophypython解决方案解决了这个同步问题:如果你(代理)不能及时获得两个叉子,你可以释放你已经拥有的任何叉子,以便另一个代理可以获得两个。这种方法不排除其他锁定方法,如锁定顺序,或涉及进程同步的系统设计,如使用信号量的生产者-消费者模型,但在Python中可能不像在其他语言中那样普遍。陷阱:Alienmethodsanddependencies如果你想在Python应用中应用多线程,那么你想要保证整个堆栈的正确性,你必须手动验证线程模型来验证线程安全和依赖。一些设计用于企业级多服务环境的依赖项,例如redis,可以在设计阶段首先考虑它们的并发模型(参见HackerNewsantirez对redis多线程版本的评论)。有些依赖项可能不会;我可能在使用boto2使用multiprocessing.pool.Pool从S3并行下载文件时遇到了死锁,这需要重写一个函数。因此,依赖性的另一个困难出现了;它们不能被同化,这意味着如果你在你的应用使用依赖模型之前不验证所有将要使用的依赖,那么当你试图为特定用途添加依赖时,你就有可能陷入项目死胡同。多线程日志如果您选择使用Python中的原生线程模型,您可能会惊喜地发现日志模块不仅是线程安全的,而且还支持从任何特定线程或进程进行日志记录(日志手册中的示例)。那么,困难的部分是在您的应用程序中哪些地方更可能触发异常,这将如何影响您的线程模型并确保围绕这些代码片段进行可靠的日志记录。将日志记录添加到您的应用程序会引入相当大的延迟,因为pylint通过警告模块logging-lazy-interpolation通知您,这也会导致您的线程模型出现问题。concurrent.futures在撰写本文时发现Pythonmultiprocessing.pool.ThreadPool实现从未被记录或测试过,因为它从未完成,这让我非常不高兴。它在Python3.7中仍然存在,因为它出现在GitHub镜像的源代码中。鉴于全局解释器锁的普遍存在,以及未来的并发程序大多是并行I/O相关工作的事实,使用新的异步模式(如concurrent.futures.Executor或Python3.x中提供的类似模式)可能更有意义,因为它们更全面。我没有使用过这个模块,但我想与多处理相比,它不会对性能造成重大影响。结论Python对线程和锁有基本的支持,可能不如Java等其他语言的线程和锁功能全面和好用。在像Python这样的高级解释型语言中操作时,最好避免使用线程和锁。然而,Python确实对线程和锁提供了足够友好的介绍,可以成为线程和锁如何工作的良好学术练习,以及对并发世界的令人兴奋的介绍。英文原文:https://bytes.yingw787.com/posts/2019/01/12/concurrency_with_python_threads_and_locks/译者:南宫云瑶