Python 中的多线程示例:在 Python 中学习 GIL
python 编程语言允许您使用多处理或多线程。在本教程中,您将学习如何使用 Python 编写多线程应用程序。
什么是线程?
线程是并发编程的执行单元。多线程是一种允许 CPU 同时执行一个进程的多个任务的技术。这些线程可以单独执行,同时共享它们的进程资源。
什么是进程?
进程基本上是正在执行的程序。当您在计算机中启动应用程序(如浏览器或文本编辑器)时,操作系统会创建一个 进程。
什么是 Python 中的多线程?
Python 中的多线程 编程是一种众所周知的技术,其中一个进程中的多个线程与主线程共享它们的数据空间,这使得线程内的信息共享和通信变得容易和高效。线程比进程轻。多线程可以单独执行,同时共享它们的进程资源。多线程的目的是同时运行多个任务和函数单元。
在本教程中,您将学习,
- 什么是线程?
- 什么是流程?
- 什么是多线程?
- 什么是多处理?
- Python 多线程与多处理
- 为什么要使用多线程?
- Python 多线程
- 线程和线程模块
- 线程模块
- 线程模块
- 死锁和竞争条件
- 同步线程
- 什么是 GIL?
- 为什么需要 GIL?
什么是多处理?
多处理允许您同时运行多个不相关的进程。这些进程不共享资源并通过 IPC 进行通信。
Python 多线程与多处理
要了解进程和线程,请考虑以下情况:计算机上的 .exe 文件是一个程序。当您打开它时,操作系统会将其加载到内存中,然后 CPU 会执行它。现在正在运行的程序的实例称为进程。
每个流程都有两个基本组成部分:
- 守则
- 数据
现在,一个进程可以包含一个或多个称为线程的子部分。 这取决于操作系统架构。您可以将线程视为可以由操作系统单独执行的进程的一部分。
换句话说,它是一个可以由操作系统独立运行的指令流。单个进程中的线程共享该进程的数据,并旨在协同工作以促进并行性。
为什么要使用多线程?
多线程允许您将应用程序分解为多个子任务并同时运行这些任务。如果你正确使用多线程,你的应用程序速度、性能和渲染都可以得到提高。
Python 多线程
Python 支持多处理和多线程的构造。在本教程中,您将主要关注实现 多线程 使用 python 的应用程序。有两个主要模块可用于处理 Python 中的线程:
- 线程 模块,以及
- 线程 模块
但是,在 python 中,还有一种叫做全局解释器锁 (GIL) 的东西。它不会带来太多的性能提升,甚至可能降低 一些多线程应用程序的性能。您将在本教程的后续部分中了解有关它的所有内容。
线程和线程模块
您将在本教程中学习的两个模块是 线程模块 和线程模块 .
但是,线程模块早已被弃用。从 Python 3 开始,它已被指定为过时的并且只能作为 __thread 访问 为了向后兼容。
您应该使用更高级别的 threading 您打算部署的应用程序的模块。此处仅出于教育目的介绍线程模块。
线程模块
使用该模块创建新线程的语法如下:
thread.start_new_thread(function_name, arguments)
好的,现在您已经了解了开始编码的基本理论。因此,打开您的 IDLE 或记事本并输入以下内容:
import time import _thread def thread_test(name, wait): i = 0 while i <= 3: time.sleep(wait) print("Running %s\n" %name) i = i + 1 print("%s has finished execution" %name) if __name__ == "__main__": _thread.start_new_thread(thread_test, ("First Thread", 1)) _thread.start_new_thread(thread_test, ("Second Thread", 2)) _thread.start_new_thread(thread_test, ("Third Thread", 3))
保存文件并按 F5 运行程序。如果一切都正确完成,这就是您应该看到的输出:
您将在接下来的部分中了解有关竞态条件以及如何处理它们的更多信息
代码说明
- 这些语句导入时间和线程模块,用于处理 Python 线程的执行和延迟。
- 在这里,您定义了一个名为 thread_test 的函数, 这将由 start_new_thread 调用 方法。该函数运行一个while循环进行四次迭代并打印调用它的线程的名称。迭代完成后,它会打印一条消息,说明线程已完成执行。
- 这是程序的主要部分。在这里,您只需调用 start_new_thread thread_test 的方法 函数作为参数。这将为您作为参数传递的函数创建一个新线程并开始执行它。请注意,您可以替换此(线程_ test) 与您想作为线程运行的任何其他函数。
线程模块
这个模块是python中线程的高级实现,也是管理多线程应用程序的事实标准。与线程模块相比,它提供了广泛的功能。

以下是该模块中定义的一些有用函数的列表:
函数名 | 说明 |
---|---|
activeCount() | 返回线程数 还活着的对象 |
currentThread() | 返回 Thread 类的当前对象。 |
枚举() | 列出所有活动的线程对象。 |
isDaemon() | 如果线程是守护进程,则返回 true。 |
isAlive() | 如果线程还活着,则返回 true。 |
线程类方法 | |
开始() | 启动线程的活动。每个线程只能调用一次,因为多次调用会抛出运行时错误。 |
运行() | 此方法表示线程的活动,并且可以被扩展 Thread 类的类覆盖。 |
join() | 它会阻止其他代码的执行,直到调用 join() 方法的线程终止。 |
背景故事:线程类
在开始使用threading模块编写多线程程序之前,了解Thread类至关重要。thread类是python中定义模板和线程操作的主要类。
创建多线程 python 应用程序最常见的方法是声明一个扩展 Thread 类并覆盖它的 run() 方法的类。
总而言之,Thread 类表示在单独的 thread 中运行的代码序列 控制权。
因此,在编写多线程应用程序时,您将执行以下操作:
- 定义一个扩展 Thread 类的类
- 覆盖 __init__ 构造函数
- 重写 run() 方法
一旦创建了一个线程对象,start() 方法可用于开始执行此活动和 join() 方法可用于阻塞所有其他代码,直到当前活动完成。
现在,让我们尝试使用 threading 模块来实现您之前的示例。再次启动你的 IDLE 并输入以下内容:
import time import threading class threadtester (threading.Thread): def __init__(self, id, name, i): threading.Thread.__init__(self) self.id = id self.name = name self.i = i def run(self): thread_test(self.name, self.i, 5) print ("%s has finished execution " %self.name) def thread_test(name, wait, i): while i: time.sleep(wait) print ("Running %s \n" %name) i = i - 1 if __name__=="__main__": thread1 = threadtester(1, "First Thread", 1) thread2 = threadtester(2, "Second Thread", 2) thread3 = threadtester(3, "Third Thread", 3) thread1.start() thread2.start() thread3.start() thread1.join() thread2.join() thread3.join()
当你执行上面的代码时,这将是输出:
代码说明
- 这部分与我们之前的示例相同。在这里,您导入用于处理 Python 线程的执行和延迟的时间和线程模块。
- 在这里,您将创建一个名为 threadtester 的类,它继承或扩展了 Thread 线程模块的类。这是在 python 中创建线程的最常见方法之一。但是,您应该只覆盖构造函数和 run() 应用程序中的方法。正如您在上面的代码示例中看到的,__init__ 方法(构造函数)已被覆盖。同样,您还覆盖了 run() 方法。它包含您要在线程内执行的代码。在本例中,您调用了 thread_test() 函数。
- 这是 thread_test() 方法,它采用 i 的值 作为参数,在每次迭代时将其减 1,然后循环其余代码,直到 i 变为 0。在每次迭代中,它打印当前执行线程的名称并休眠等待秒(这也被用作参数)。
- thread1 =threadtester(1, “First Thread”, 1) 在这里,我们正在创建一个线程并传递我们在 __init__ 中声明的三个参数。第一个参数是线程的id,第二个参数是线程的名字,第三个参数是计数器,它决定了while循环应该运行多少次。
- thread2.start()start方法用于启动线程的执行。在内部,start() 函数调用类的 run() 方法。
- thread3.join() join() 方法会阻塞其他代码的执行,并等待调用它的线程完成。
如您所知,同一进程中的线程可以访问该进程的内存和数据。因此,如果多个线程同时尝试更改或访问数据,则可能会出现错误。
在下一节中,您将看到当线程访问数据和临界区而不检查现有访问事务时可能出现的各种复杂情况。
死锁和竞争条件
在了解死锁和竞争条件之前,了解一些与并发编程相关的基本定义会很有帮助:
- Critical Section是访问或修改共享变量的代码片段,必须作为原子事务执行。
- 上下文切换是 CPU 在从一个任务切换到另一个任务之前存储线程状态的过程,以便以后可以从同一点恢复。
死锁
死锁是开发人员在 python 中编写并发/多线程应用程序时最担心的问题。理解死锁的最佳方法是使用经典的计算机科学示例问题,即餐饮哲学家问题。
哲学家用餐问题陈述如下:
如图所示,五位哲学家坐在一张圆桌上,桌上摆着五盘意大利面(一种意大利面)和五把叉子。

在任何给定时间,哲学家必须要么在吃饭,要么在思考。
此外,哲学家必须拿走他旁边的两个叉子(即左右叉子)才能吃意大利面。当所有五个哲学家同时拿起他们的右叉时,就会出现死锁问题。
由于每个哲学家都有一个叉子,他们都会等待其他人放下叉子。结果,他们都不能吃意大利面了。
同样,在并发系统中,当不同的线程或进程(哲学家)试图同时获取共享的系统资源(分叉)时,就会发生死锁。结果,没有一个进程有机会执行,因为它们正在等待其他进程持有的另一个资源。
比赛条件
竞争条件是当系统同时执行两个或多个操作时出现的程序的不想要的状态。例如,考虑这个简单的 for 循环:
i=0; # a global variable for x in range(100): print(i) i+=1;
如果你创建 n 一次运行此代码的线程数,当程序完成执行时,您无法确定 i 的值(由线程共享)。这是因为在真正的多线程环境中,线程是可以重叠的,一个线程检索和修改的 i 的值会在其他线程访问它之间发生变化。
这是多线程或分布式 python 应用程序中可能出现的两类主要问题。在下一节中,您将学习如何通过同步线程来解决这个问题。
同步线程
为了处理竞争条件、死锁和其他基于线程的问题,threading 模块提供了 Lock 目的。这个想法是,当线程想要访问特定资源时,它会获取该资源的锁。一旦一个线程锁定了一个特定的资源,在释放锁之前没有其他线程可以访问它。因此,对资源的更改将是原子的,并且会避免竞争条件。
锁是由 __thread 实现的低级同步原语 模块。在任何给定时间,锁都可以处于以下两种状态之一:锁定 或解锁。 它支持两种方法:
- 获取() 当锁定状态被解锁时,调用acquire() 方法会将状态更改为锁定并返回。但是,如果状态被锁定,acquire() 的调用将被阻塞,直到 release() 方法被其他线程调用。
- 释放() release() 方法用于将状态设置为未锁定,即释放锁。它可以被任何线程调用,不一定是获得锁的线程。
这是在您的应用程序中使用锁的示例。启动你的 IDLE 并输入以下内容:
import threading lock = threading.Lock() def first_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the first funcion') lock.release() def second_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the second funcion') lock.release() if __name__=="__main__": thread_one = threading.Thread(target=first_function) thread_two = threading.Thread(target=second_function) thread_one.start() thread_two.start() thread_one.join() thread_two.join()
现在,按 F5。你应该会看到这样的输出:
代码说明
- 在这里,您只是通过调用 threading.Lock() 创建一个新锁 工厂功能。在内部,Lock() 返回由平台维护的最有效的具体 Lock 类的实例。
- 在第一条语句中,您通过调用acquire() 方法来获取锁。当锁被授予时,你打印“锁获得” 到控制台。一旦您希望线程运行的所有代码执行完毕,您就可以通过调用 release() 方法来释放锁。
理论很好,但你怎么知道锁真的有效?如果您查看输出,您将看到每个打印语句一次只打印一行。回想一下,在前面的示例中, print 的输出很随意,因为多个线程同时访问 print() 方法。这里,打印函数只有在获得锁之后才会被调用。因此,输出是一次显示一个并逐行显示。
除了锁之外,python 还支持一些其他机制来处理线程同步,如下所示:
- RLocks
- 信号量
- 条件
- 事件和
- 障碍
全局解释器锁(以及如何处理)
在深入了解 python 的 GIL 的细节之前,让我们定义一些有助于理解下一节的术语:
- CPU 绑定代码:这是指将由 CPU 直接执行的任何代码。
- I/O 绑定代码:这可以是通过操作系统访问文件系统的任何代码
- CPython:它是参考实现 Python的,可以说是用C和Python(编程语言)编写的解释器。
什么是 Python 中的 GIL?
全局解释器锁 (GIL) 在 python 中是处理进程时使用的进程锁或互斥锁。它确保一个线程一次可以访问特定资源,并且还防止同时使用对象和字节码。这有利于单线程程序的性能提升。 python中的GIL非常简单,易于实现。
锁可用于确保在给定时间只有一个线程可以访问特定资源。
Python 的一个特点是它对每个解释器进程使用了全局锁,这意味着每个进程都将 Python 解释器本身视为一种资源。
例如,假设您编写了一个 python 程序,它使用两个线程来执行 CPU 和“I/O”操作。当你执行这个程序时,会发生这样的事情:
- python 解释器创建一个新进程并产生线程
- 当 thread-1 开始运行时,它会首先获取 GIL 并锁定它。
- 如果线程 2 现在想执行,即使另一个处理器空闲,它也必须等待 GIL 被释放。
- 现在,假设线程 1 正在等待 I/O 操作。这时,它会释放 GIL,thread-2 会获取它。
- 在完成 I/O 操作后,如果 thread-1 想要现在执行,它必须再次等待 GIL 被 thread-2 释放。
因此,任何时候只有一个线程可以访问解释器,这意味着在给定的时间点将只有一个线程执行 python 代码。
这在单核处理器中没问题,因为它将使用时间片(参见本教程的第一部分)来处理线程。但是,在多核处理器的情况下,在多个线程上执行的受 CPU 限制的函数将对程序的效率产生相当大的影响,因为它实际上不会同时使用所有可用的内核。
为什么需要 GIL?
CPython 垃圾收集器使用称为引用计数的高效内存管理技术。它是这样工作的:python 中的每个对象都有一个引用计数,当它被分配给一个新的变量名或添加到一个容器(如元组、列表等)时,它会增加。同样,当引用超出范围或调用 del 语句时,引用计数会减少。当对象的引用计数为 0 时,进行垃圾回收,并释放分配的内存。
但问题是引用计数变量像任何其他全局变量一样容易出现竞争条件。为了解决这个问题,python 的开发者决定使用全局解释器锁。另一种选择是为每个对象添加一个锁,这会导致死锁并增加 acquire() 和 release() 调用的开销。
因此,对于运行繁重的 CPU 绑定操作(有效地使它们成为单线程)的多线程 python 程序,GIL 是一个重大限制。如果您想在应用程序中使用多个 CPU 内核,请使用 multiprocessing 代替模块。
总结
- Python 支持 2 个多线程模块:
- __thread 模块:它为线程提供了低级实现,已过时。
- 线程模块 :它为多线程提供了高级实现,是当前的标准。
- 要使用 threading 模块创建线程,您必须执行以下操作:
- 创建一个扩展 Thread 的类 类。
- 覆盖其构造函数 (__init__)。
- 覆盖它的 run() 方法。
- 创建这个类的一个对象。
- 一个线程可以通过调用start()来执行 方法。
- join() 方法可用于阻塞其他线程,直到该线程(调用 join 的线程)完成执行。
- 当多个线程同时访问或修改共享资源时,就会出现竞争条件。
- 可以通过同步线程来避免。
- Python 支持 6 种方式来同步线程:
- 锁
- RLocks
- 信号量
- 条件
- 事件和
- 障碍
- 锁只允许获得锁的特定线程进入临界区。
- 锁有两种主要方法:
- 获取() :它将锁定状态设置为 locked。 如果在锁定的对象上调用,它会阻塞直到资源空闲。
- 释放() :它将锁定状态设置为解锁 并返回。如果在未锁定的对象上调用,则返回 false。
- 全局解释器锁是一种机制,一次只能执行 1 个 CPython 解释器进程。
- 它用于促进 CPythons 垃圾收集器的引用计数功能。
- 要使 Python 应用具有繁重的 CPU 密集型操作,您应该使用多处理模块。
Python