Menu Close

Python GIL全局解释器锁

python编程语言允许您使用多处理或多线程。在本教程中,您将学习如何在Python中编写多线程应用程序。

线程是什么?

线程是并行编程执行的一个单元。多线程技术是一种允许CPU同时执行一个进程的许多任务的技术。这些线程可以在共享进程资源的同时单独执行。

进程是什么?

进程基本上就是正在执行的程序。当您在计算机中启动应用程序(例如浏览器或文本编辑器)时,操作系统将创建一个进程。

Python中的多线程是什么?

Python编程中的多线程是一种众所周知的技术,其中一个进程中的多个线程与主线程共享其数据空间,这使线程内的信息共享和通信变得容易且高效。线程比进程轻。多线程可以在共享进程资源的同时单独执行。多线程的目的是同时运行多个任务和功能单元。

什么是多处理?

多重处理使您可以同时运行多个不相关的进程。这些过程不共享资源,也不通过IPC进行通信。

Python多线程与多处理

若要了解进程和线程,请考虑以下情形:您计算机上的.exe文件是一个程序。当您打开它时,操作系统会将其加载到内存中,然后CPU会执行它。现在正在运行的程序实例称为进程。

每个过程都将包含2个基本组成部分:

  • 代码
  • 数据

现在,一个进程可以包含一个或多个称为线程的子部分这取决于操作系统体系结构。您可以将线程视为进程的一部分,该线程可以由操作系统单独执行。

换句话说,它是可以由OS独立运行的指令流。单个进程中的线程共享该进程的数据,并设计为协同工作以促进并行性。

为什么要使用多线程?

多线程允许您将应用程序分解为多个子任务,并同时运行这些任务。如果正确使用多线程,则可以提高应用程序的速度,性能和呈现能力。

Python多线程

Python支持多处理以及多线程的构造。在本教程中,您将主要侧重于使用python实现多线程应用程序。有两个主要模块可用于处理Python中的线程:

  1. thread       模块
  2. threading 模块

thread和threading模块

在本教程中您将学习的两个模块

但是,thread模块早就被弃用了。从Python3开始,它被指定为过时的,并且只能通过 _thread 进行访问,以实现向后兼容性。

对于要部署的应用程序,应该使用更高级的线程模块。线程模块只是出于了解目的的在这里介绍的。

thread模块

使用此模块创建新线程的语法如下:

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 执行完毕" % name)


if __name__ == "__main__":

    _thread.start_new_thread(thread_test, ("线程1", 1))
    _thread.start_new_thread(thread_test, ("线程2", 2))
    _thread.start_new_thread(thread_test, ("线程3", 3))

新建python文件,填入代码,然后按F5键运行程序:

%title插图%num

代码说明

  1. 导入模块,用于处理Python线程的执行和延迟。
  2. 定义一个名为thread_test的函数该函数将由start_new_thread方法调用。该函数运行while循环进行四次迭代,并输出调用它的线程的名称。一旦迭代完成,它将打印一条消息,直到线程完成执行。
  3. 使用thread_test函数作为参数来调用start_new_thread方法。这将为您作为参数传递的函数创建一个新线程,并开始执行它。请注意,您可以将其替换为要作为线程运行的任何其他函数。

threading模块

该模块是python中thread的高级实现,也是用于管理多线程应用程序的标准。与thread模块相比,它提供了广泛的功能。

线程模块结构
线程模块结构

以下是此模块中定义的一些有用方法的列表:

函数 描述
activeCount() 返回当前存活的 Thread 对象的数量。 返回值与 enumerate() 所返回的列表长度一致。
currentThread() 返回当前对应调用者的控制线程的 Thread 对象。如果调用者的控制线程不是利用 threading 创建,会返回一个功能受限的虚拟线程对象。
enumerate() 列出所有活动的Thread对象。
isDaemon() 如果线程是守护程序,则返回true。
isAlive() 返回线程是否存活。
线程类方法
start() 启动线程的活动。每个线程只能调用一次,因为如果多次调用它将抛出运行时错误。
run() 此方法表示线程的活动,并且可以由扩展Thread类的类覆盖。
join() 它阻止其他代码的执行,直到调用join方法的线程终止为止。

线程类:

在开始使用线程模块对多线程程序进行编码之前,了解Thread类至关重要。thread类是主要类,它定义了python中的线程的模板和操作。

创建多线程python应用程序的最常见方法是声明一个继承Thread并覆盖其run方法的类。

总而言之,Thread类表示在单独的控制线程中运行的代码序列。

因此,在编写多线程应用程序时,您将执行以下操作:

  1. 定义一个继承Thread的类
  2. 重写__init__构造函数
  3. 重写run方法

创建线程对象后,可以使用start方法开始执行此活动,而可以使用join方法阻止所有其他代码,直到当前活动结束为止。

现在,让我们尝试使用线程模块来实现您先前的示例。同样,启动您的IDLE并输入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 执行完毕 " % 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, "线程1", 1)
    thread2 = threadtester(2, "线程2", 2)
    thread3 = threadtester(3, "线程3", 3)

    thread1.start()
    thread2.start()
    thread3.start()

    thread1.join()
    thread2.join()
    thread3.join()

代码说明

  1. 这部分与我们前面的示例相同。在这里,您将导入模块,该模块用于处理Python线程的执行和延迟。
  2. 在这一部分中,您将创建一个名为threadtester的类,该类继承或扩展了线程模块的Thread类。这是在python中创建线程的最常见方法之一。但是,您只应在应用程序中覆盖构造函数和run方法。如您在上面的代码示例中看到的那样,__init__()已被覆盖。同样,您也重写了run方法。它包含您要在线程内执行的代码。在此示例中,您已调用thread_test函数。
  3. 这是thread_test()方法,该方法将i的值作为参数,在每次迭代时将其减少1,并循环遍历其余代码,直到i变为0。在每次迭代中,它都会打印当前正在执行的线程的名称。并等待数秒(也被视为一个参数)。
  4. thread1 = threadtester(1, “线程1”, 1) 这里,我们正在创建一个线程,并传递在__init__中声明的三个参数。第一个参数是线程的ID,第二个参数是线程的名称,第三个参数是计数器,它确定while循环应运行多少次。
  5. thread2.start()start方法用于启动线程的执行。在内部,start()函数调用您的类的run方法。
  6. thread3.join()join方法阻止其他代码的执行,并等待直到调用它的线程完成。

如您所知,处于同一进程中的线程可以访问该进程的内存和数据。结果,如果一个以上的线程试图同时更改或访问数据,则可能出现错误。

在下一部分中,您将看到线程在访问数据和关键部分而不检查现有访问事务时可能会显示的各种复杂情况。

死锁和竞争条件

在学习死锁和竞争条件之前,了解一些与并发编程相关的基本定义将是有帮助的:

  • 临界区它是访问或修改共享变量的代码片段,必须作为原子事务执行。
  • 上下文切换这是CPU在从一个任务更改为另一个任务之前遵循的存储线程状态的过程,以便以后可以从同一点恢复它。

死锁

死锁是开发人员在python中编写并发/多线程应用程序时最担心的问题。理解死锁的最佳方法是使用经典的计算机科学示例问题,即哲学家进餐问题。

问题陈述如下:

如图所示,五个哲学家坐在一张圆桌上,上面放着五盘意大利面和五把叉子。

%title插图%num

哲学家的问题

在任何给定的时间,哲学家要么在吃东西,要么在思考。

此外,哲学家在吃意大利面之前必须拿起与他相邻的两个叉子(即左叉和右叉)。当所有五个哲学家同时拿起他们的右叉时,就会出现死锁的问题。

由于每个哲学家都有一把叉子,所以他们都将等待其他哲学家放下叉子。结果,他们都无法吃意大利面。

同样,在并发系统中,当不同的线程或进程(哲学家)试图同时获取共享的系统资源(fork)时,就会发生死锁。结果,在等待其他进程拥有的另一资源时,这些进程都没有机会执行。

竞争条件

竞争条件是程序的有害状态,当系统同时执行两个或多个操作时会发生竞争状态。例如,考虑以下简单的for循环:

i=0; 
for x in range(100):
    print(i)
    i+=1;

如果创建n个线程一次运行此代码,则无法确定程序完成执行时i的值(该值由线程共享)。这是因为在实际的多线程环境中,线程可能会重叠,并且在其他某个线程访问它时,由线程检索和修改的i的值可能会在这两者之间发生变化。

这是在多线程或分布式python应用程序中可能发生的两大类问题。在下一节中,您将学习如何通过同步线程来克服此问题。

同步线程

为了处理竞争条件,死锁和其他基于线程的问题,线程模块提供了Lock对象。其思想是,当线程想要访问特定资源时,它将获取该资源的锁。一旦线程锁定了特定资源,在释放锁定之前,其他线程将无法访问该资源。结果,对资源的更改将是原子的,并且避免了竞争条件。

锁是由__thread模块实现的低级同步原语。在任何给定时间,锁可以处于以下两种状态之一:锁定非锁定。它支持两种方法:

  1. acquire() 当锁定状态被解锁时,调用acquire方法会将状态更改为锁定并返回。但是,如果该状态为锁定,则对acquire()的调用将被阻止,直到其他某个线程调用release()方法为止。
  2. release() 方法用于将状态设置为解锁,即释放锁。可以由任何线程调用它,不一定是获得该锁的线程。

这是在您的应用程序中使用锁的示例。启动您的IDLE并输入以下内容:

import threading

lock = threading.Lock()


def first_function():
    for i in range(100):
        lock.acquire()
        print('获取锁')
        print('执行第一个函数')
        lock.release()


def second_function():
    for i in range(100):
        lock.acquire()
        print('获取锁')
        print('执行第二个函数')
        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。您应该看到类似以下的输出:

 

代码说明

  1. 在这里,您只需通过调用threading.Lock()工厂函数来创建新锁。在内部,Lock返回由平台维护的最有效的具体Lock类的实例。
  2. 在第一条语句中,您可以通过调用acquire方法来获取锁。授予锁定后,将“获取锁”打印到控制台。一旦您希望线程运行的所有代码完成执行,您就可以通过调用release方法来释放锁。

理论上还不错,但是您如何知道该锁确实起作用?如果查看输出,您将看到每个打印语句一次仅打印一行。回想一下,在较早的示例中,由于多个线程同时访问print方法,因此print的输出很随意。在此,仅在获得锁定后才调用打印功能。因此,输出一次一行一行地显示。

除了锁之外,python还支持其他一些机制来处理线程同步,如下所示:

  1. RLocks
  2. Semaphores
  3. Conditions
  4. Events, and
  5. Barriers

全局解释器锁

在详细介绍python的GIL之前,让我们定义一些术语,这些术语将有助于理解后面的部分:

  1. CPU绑定代码:这是指将直接由CPU执行的任何代码段。
  2. I / O绑定代码:这可以是通过OS访问文件系统的任何代码
  3. CPython:它是Python的参考实现,可以描述为用C和Python编写的解释器。

什么是Python中的GIL?

python中的全局解释器锁(GIL)是在处理进程时使用的进程锁或互斥锁。它确保一个线程一次可以访问特定资源,并且还可以防止一次使用对象和字节码。这有利于单线程程序提高性能。python中的GIL非常简单且易于实现。

锁可用于确保在给定时间只有一个线程可以访问特定资源。

Python的功能之一是它在每个解释器进程上使用全局锁,这意味着每个进程都将python解释器本身视为资源。

例如,假设您编写了一个python程序,该程序使用两个线程来执行CPU和“ I / O”操作。当您执行此程序时,将发生以下情况:

  1. python解释器创建一个新进程并产生线程
  2. 当线程1开始运行时,它将首先获取GIL并将其锁定。
  3. 如果线程2要立即执行,则即使另一个处理器空闲,它也必须等待GIL被释放。
  4. 现在,假设线程1正在等待I / O操作。这时,它将释放GIL,线程2将获取它。
  5. 完成I / O操作后,如果线程1要立即执行,它将不得不再次等待线程2释放GIL。

因此,在任何时间只有一个线程可以访问解释器,这意味着在给定的时间点只有一个线程执行python代码。

在单核处理器中,这是可以的,因为它将使用时间分片(请参阅本教程的第一部分)来处理线程。但是,在多核处理器的情况下,在多个线程上执行的CPU绑定功能将对程序的效率产生重大影响,因为它实际上不会同时使用所有可用的内核。

为什么需要GIL?

CPython垃圾收集器使用一种称为引用计数的有效内存管理技术。它是这样工作的:python中的每个对象都有一个引用计数,当将其分配给新的变量名称或添加到容器(如元组,list等)时,引用计数就会增加。同样,当引用超出范围或调用del语句时,引用计数也会减少。当对象的引用计数达到0时,将对其进行垃圾回收,并释放已分配的内存。

但是问题在于,引用计数变量与其他任何全局变量一样,也容易出现竞争条件。为了解决这个问题,python的开发人员决定使用全局解释器锁。另一个选择是向每个对象添加一个锁,这将导致死锁,并增加来自acquire和release调用的开销。

因此,对于运行繁重的CPU绑定操作(有效地使它们成为单线程)的多线程python程序,GIL是一个重大限制。如果要在应用程序中使用多个CPU内核,请改用多处理模块。

 

python系列教程目录

Posted in Python

发表评论

相关链接