python编程语言允许您使用多处理或多线程。在本教程中,您将学习如何在Python中编写多线程应用程序。
线程是并行编程执行的一个单元。多线程是一种允许CPU同时执行一个进程的许多任务的技术。这些线程可以在共享进程资源的同时单独执行。
进程基本上就是正在执行的程序。当您在计算机中启动应用程序(例如浏览器或文本编辑器)时,操作系统将创建一个进程。
PYTHON编程中的多线程处理是一项众所周知的技术,其中进程中的多个线程与主线程共享其数据空间,这使线程内的信息共享和通信变得容易而高效。线程比进程轻。多线程可以在共享进程资源的同时单独执行。多线程的目的是同时运行多个任务和功能单元。
多重处理使您可以同时运行多个不相关的进程。 这些过程不共享资源,也不通过IPC进行通信。
若要了解进程和线程,请考虑以下情形:您计算机上的.exe文件是一个程序。 当您打开它时,操作系统将其加载到内存中,然后CPU执行它。 现在正在运行的程序实例称为进程。
每个过程将包含2个基本组成部分:
现在,一个进程可以包含一个或多个称为threads的子部分。 这取决于OS体系结构。您可以将线程视为进程的一部分,可以由操作系统单独执行。
换句话说,它是可以由OS独立运行的指令流。 单个进程中的线程共享该进程的数据,并设计为协同工作以促进并行性。
多线程允许您将应用程序分解为多个子任务,并同时运行这些任务。如果正确使用多线程,则可以提高应用程序的速度,性能和呈现能力。
Python支持多处理以及多线程的构造。在本教程中,您将主要侧重于使用python实现多线程应用程序。有两个主要模块可用于处理Python中的线程:
但是,在python中,还有一个称为全局解释器锁(GIL)的东西。它不会带来很大的性能提升,甚至可能会降低某些多线程应用程序的性能。您将在本教程的后续部分中了解所有相关信息。
您将在本教程中学习的两个模块是Thread 模块和Threading 模块。
但是,Thread 模块早已被弃用。从Python 3开始,它已被指定为过时的,并且只能通过__thread进行访问以实现向后兼容。
您应该对要部署的应用程序使用更高级别的Threading 模块。此处仅出于教育目的涵盖了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 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键运行该程序。 如果一切都正确完成,那么您应该看到以下输出:
在接下来的部分中,您将了解有关race conditions以及如何处理它们的更多信息。
代码说明
这将为您作为参数传递的函数创建一个新线程,并开始执行它。 请注意,您可以将此(thread_test)替换为要作为线程运行的任何其他函数。
该模块是python中线程的高级实现,也是用于管理多线程应用程序的事实上的标准。 与 thread module相比,它提供了广泛的功能(features )。
穿线模块的结构
以下是此模块中定义的一些有用函数的列表:
背景:线程类(The Thread Class)
在开始使用threading module编码多线程程序之前,了解Thread类至关重要。线程类是主要类,它定义了python中的线程的模板和操作。
创建多线程python应用程序的最常用方法是声明一个扩展Thread类并覆盖其run()方法的类。
总而言之,Thread类表示在单独的thread 控制中运行的代码序列。
因此,在编写多线程应用程序时,您将执行以下操作:
创建线程对象后,可以使用start()方法开始执行此活动,而可以使用join()方法阻止所有其他代码,直到当前活动结束为止。
现在,让我们尝试使用线程模块来实现您先前的示例。再次,启动您的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()
当您执行上面的代码时,这将是输出:
代码说明
同样,您也重写了run()方法。它包含您要在线程内执行的代码。在此示例中,您已调用thread_test()函数。
在这里,我们正在创建一个线程并传递在__init__中声明的三个参数。第一个参数是线程的ID, 第二个参数是线程的名称,第三个参数是计数器,它确定while循环应运行多少次。
start方法用于启动线程的执行。在内部,start()函数调用您的类的run()方法。
join()方法阻止其他代码的执行,并等待直到调用它的线程完成。
如您所知,处于同一进程中的线程可以访问该进程的内存和数据。结果,如果一个以上的线程试图同时更改或访问数据,则错误可能蔓延。
在下一节中,您将看到线程在不检查现有访问事务的情况下访问数据和关键部分时可能出现的各种复杂情况。
在学习死锁和竞争条件之前,了解一些与并发编程相关的基本定义将是有帮助的:
它是访问或修改共享变量的代码片段,必须作为原子事务执行。
这是CPU在从一个任务更改为另一任务之前遵循的存储线程状态的过程,以便以后可以从同一点恢复该线程。
死锁是开发人员在python中编写并发/多线程应用程序时最担心的问题。 理解僵局的最佳方法是使用经典的计算机科学示例问题,即进餐哲学家问题(Dining Philosophers Problem)。
餐饮哲学家的问题陈述如下:
如图所示,五个哲学家坐在一张圆桌上,上面放着五盘意大利面(一种面食)和五把叉子。
餐饮哲学家的问题
在任何给定的时间,哲学家必须在吃饭或思考。
此外,哲学家必须拿起与他相邻的两个叉子(即左右叉子)才能吃意大利面。 当所有五个哲学家同时拿起他们的右叉时,就会出现死锁的问题。
由于每个哲学家都有一把叉子,所以他们都将等待其他哲学家放下叉子。 结果,他们都无法吃意大利面。
类似地,在并发系统中,当不同的线程或进程(哲学家)试图同时获取共享的系统资源(fork)时,就会发生死锁。 结果,在等待其他进程拥有的另一资源时,这些进程都没有机会执行。
竞态条件(Race Conditions)
竞争条件是程序的有害状态,当系统同时执行两个或多个操作时会发生竞争状态。 例如,考虑以下简单的for循环:
i=0; # a global variable
for x in range(100):
print(i)
i+=1;
如果创建n个线程一次运行此代码,则无法确定程序完成执行时i的值(由线程共享)。 这是因为在实际的多线程环境中,线程可能会重叠,并且在其他某个线程访问它时,由线程检索和修改的i的值可能会在这两者之间发生变化。
这是在多线程或分布式python应用程序中可能发生的两大类问题。 在下一节中,您将学习如何通过同步线程来克服此问题。
为了处理竞争条件,死锁和其他基于线程的问题,线程模块提供了Lock对象。 其思想是,当线程想要访问特定资源时,它将获取该资源的锁。 一旦线程锁定了特定资源,在释放该锁定之前,没有其他线程可以访问该资源。 结果,对资源的更改将是原子的,并且将避免竞争条件。
锁是由__thread模块实现的低级同步原语。 在任何给定时间,锁可以处于以下两种状态之一:锁定或解锁(locked or unlocked)。 它支持两种方法:
acquire()
当锁定状态被解锁时,调用acquire()方法会将状态更改为锁定并返回。 但是,如果状态为锁定,则对acquire()的调用将被阻止,直到其他线程调用release()方法为止。
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。 您应该看到如下输出:
代码说明
理论上还不错,但是您如何知道该锁确实起作用?如果查看输出,您将看到每个打印语句一次仅打印一行。回想一下,在较早的示例中,由于多个线程同时访问print()方法,因此print的输出很随意。在此,仅在获得锁定后才调用打印函数。因此,输出一次一行一行地显示。
除了锁,python还支持其他一些机制来处理线程同步,如下所示:
全局解释器锁定(以及如何处理)
在详细介绍python的GIL之前,让我们定义一些术语,这些术语将有助于理解后面的部分:
python中的GLOBAL INTERPRETER LOCK(GIL)是处理进程时使用的进程锁或互斥锁。它确保一个线程可以一次访问特定资源,并且还可以防止一次使用对象和字节码。这有利于单线程程序提高性能。 python中的GIL非常简单且易于实现。
可以使用锁来确保在给定的时间只有一个线程可以访问特定资源。
Python的功能(features )之一是它在每个解释器进程上使用全局锁,这意味着每个进程都将python解释器本身视为资源。
例如,假设您编写了一个python程序,该程序使用两个线程来执行CPU和“ I / O”操作。当您执行此程序时,将发生以下情况:
因此,在任何时候都只有一个线程可以访问解释器,这意味着在给定的时间点只有一个线程执行python代码。
在单核处理器中这没关系,因为它将使用时间分片(请参阅本教程的第一部分)来处理线程。但是,在多核处理器的情况下,在多个线程上执行的CPU绑定函数将对程序的效率产生重大影响,因为它实际上不会同时使用所有可用的内核。
CPython垃圾收集器使用一种称为引用计数的有效内存管理技术。它是这样工作的:python中的每个对象都有一个引用计数,当它分配给新的变量名称或添加到容器(如元组,列表等)时,引用计数就会增加。同样,当引用超出范围或调用del语句时,引用计数也会减少。当对象的引用计数达到0时,将对其进行垃圾回收,并释放已分配的内存。
但是问题是,引用计数变量像任何其他全局变量一样容易出现竞争条件。为了解决这个问题,python的开发人员决定使用全局解释器锁。另一种选择是向每个对象添加一个锁,这将导致死锁,并增加acquire()和release()调用的开销。
因此,对于运行繁重的CPU绑定操作(有效地使其成为单线程)的多线程python程序,GIL是一个重大限制。如果要在应用程序中使用多个CPU内核,请改用 multiprocessing module。