threading模块从Python1.5.2版本开始出现,用于增强底层多线程模块thread。Threading模块使得多线程的操作更加简单,支持程序同时运行多个操作。请注意,Python中的多线程最适用于与I/O相关的操作,例如从Internet下载资源或读取本地文件或目录。如果你正在做的是CPU密集型的,那么你需要使用Python的多处理模块。这样做的原因是Python有一个全局解释器锁(GIL),这样所有的子线程都必须在同一个主线程中运行。正因为如此,当你多线程去处理多个CPU密集型任务时,你会发现它实际上运行得更慢。所以我们专注于多线程最擅长的方面:I/O操作!线程简介多线程允许你运行一段很长的代码,就好像它是一个单独的程序一样。这有点像调用子进程(subprocess),但不同的是你调用的是函数或类,而不是单独的程序。在我看来,举个例子会更有帮助。让我们看一个简单的例子:在这里,我们导入线程模块并创建一个名为doubler的常规函数??。此函数接受一个值并将该值加倍。它还打印调用此函数的线程的名称,并在末尾打印一个空行。然后在最后一段代码中,我们创建了五个线程,并一一启动。当我们实例化一个线程时,您会注意到我们将doubler函数传递给了target参数,同时也将参数传递给了doubler函数。Args参数看起来有点奇怪,那是因为我们需要将一个序列传递给doubler函数,但它只接受一个变量,所以我们将逗号放在末尾以创建一个只有一个参数的序列。需要注意的是,如果要等待一个线程结束,需要调用join()方法。当您运行上面的代码时,您将得到以下输出:当然,通常您不希望将输出打印到标准输出。如果不幸这样做了,最终的显示会很混乱。您应该使用Python的日志记录模块。它是线程安全的并且性能良好。让我们修改上面的示例以使用日志记录模块并为我们的线程命名。代码如下:代码中最大的变化是增加了get_logger函数。此代码将创建一个设置为调试级别的记录器。它将日志保存在当前目录(即脚本运行的目录)中,然后对日志的每一行进行格式化。格式包括时间戳、线程名称、日志级别和日志信息。在doubler函数中,我们将print语句替换为logging语句。您会注意到,在创建线程时,我们将logger对象传递给了doubler函数。这样做的原因是,如果在每个线程中实例化日志对象,就会有多个日志单例,日志中就会有很多重复。最后,创建一个名称列表,然后使用name关键字参数为每个线程设置具体的名称,这样线程就可以命名了。运行上面的代码应该会产生一个包含以下内容的日志文件:输出是不言自明的,所以继续。这节再多说一点,就是通过继承threading.Thread来实现多线程。最后一个例子,通过继承threading.Thread创建一个子类,而不是直接调用Thread函数。更新后的代码如下:在这个例子中,我们只是创建了一个继承自threading.Thread的子类。和以前一样,将一个数字传递给double和logging对象。但是这次设置线程名称的方式有点不同,变成了调用线程对象的setName方法来设置。我们仍然需要调用start来启动线程,但是您可能会注意到我们不需要在子类中定义这个方法。调用start时,它通过调用run方法启动线程。在我们的类中,我们调用doubler函数来进行处理。除了添加了一些附加信息外,输出几乎相同。运行这个脚本,看看你得到了什么。线程锁和线程同步当你有多个线程时,你需要考虑如何避免线程冲突。我的意思是你可能有多个线程同时访问同一个资源。如果不思考这些问题并据此制定解决方案,那么在产品开发过程中,总会在最糟糕的时候遇到这些棘手的问题。解决方案是使用线程锁。锁由Python的线程模块提供,最多由一个线程持有。当线程试图获取已锁定在资源上的锁时,线程通常会暂停执行,直到锁被释放。我们来看一个非常典型的没有锁功能但应该有锁功能的例子:如果在上面的代码中加入time.sleep函数,并给出不同的时间长度,可能会让这个例子更有趣。不管怎样,这里的问题是一个线程可能已经调用了update_total函数,更新还没有完成,另一个线程可能调用它并尝试更新内容。根据执行操作的顺序,此值可能只会增加一次。让我们给这个函数加一把锁。有两种方法可以实现这一点。第一种方式是使用try/finally来保证一定会释放锁。下面是一个示例:如上所述,在我们进行任何处理之前获取锁。然后尝试更新total的值,最后释放锁并打印出total的当前值。其实我们可以使用Python的with语句来避免更繁琐的try/finally语句:如你所见,我们不再需要try/finally作为上下文管理器,而是使用with语句。当然,你也会遇到在代码中希望通过多线程访问多个函数的情况。当您第一次编写并发代码时,代码可能看起来像这样:这样的代码在上述情况下工作正常,但假设您有多个线程调用这两个函数。当一个线程运行这两个函数时,另一个线程可能会修改数据,最终得到不正确的结果。问题是,您甚至可能不会立即意识到结果是错误的。解决办法是什么?让我们试着找出答案。通常首先想到的是锁定这两个函数调用的地方。让我们尝试将上面的示例修改为如下所示:当您实际运行这段代码时,您会发现它只是挂起。原因是因为我们只告诉线程模块获取锁。所以当我们调用第一个函数时,它看到锁已经被获取,并挂起自己直到锁被释放,但这种情况从未发生过。真正的解决办法是使用可重入锁(Re-EntrantLock)。threading模块提供的解决方案是使用RLock函数。即把lock=threading.lock()换成lock=threading.RLock(),然后重新运行代码,现在代码可以正常运行了。如果要在线程中运行上面的代码,那么不用直接调用main函数,可以使用如下代码:每个线程都会运行main函数,main函数依次调用另外两个函数。最终也会产生10组结果集。计时器Threading模块有一个优雅的Timer类,您可以使用它来实现在指定时间后发生的操作。他们实际上启动了自己的自定义线程,这些线程通过在常规线程上调用start()方法来运行。您也可以调用其取消方法来停止计时器。值得注意的是,您甚至可以在计时器开始之前取消它。有一天,我遇到了一个特殊情况:我需要和一个已经启动的子进程通信,但是我需要它有一个超时时间。虽然有许多不同的方法来处理这个特定的问题,但我最喜欢的解决方案是使用线程模块的Timer类。在下面的示例中,我们将使用ping命令进行演示。在Linux系统上,ping命令将一直运行,直到您手动将其终止。所以在Linux的世界里,Timer类是非常方便的。下面是一个示例:这里我们在lambda表达式中调用kill来终止进程。接下来启动ping命令,然后创建Timer对象。你会注意到第一个参数是等待的秒数,第二个参数是要调用的函数,next参数是要调用的函数的入参。在这种情况下,我们的函数是一个lambda表达式,它传递一个包含一个元素的列表。如果运行这段代码,它应该运行5秒,然后打印出ping的结果。附加线程组件Threading模块包含对附加功能的支持。例如,您可以创建信号量(Semaphore),它是计算机科学中最古老的同步原语之一。基本上,一个信号量管理一个内置计数器。调用acquire时计数器递减,调用release时计数器递增。按照设计,计数器的值不能小于零,因此如果恰好在计数器为零时调用acquire方法,该方法将阻塞线程。译者注:通常在使用信号量时,会初始化一个大于零的值,比如semaphore=threading.Semaphore(2)另一个非常有用的同步工具是事件(Event)。它允许您使用信号在线程之间进行通信。在下一节中,我们将给出一个使用事件的示例。最后,在Python3.2中添加了Barrier对象。Barrier是管理线程池中的一个同步原语。在线程池中,多个线程需要相互等待。如果通过了屏障,则每个线程都必须调用wait()方法,该线程将阻塞,直到其他线程调用此方法。所有调用结束后,所有线程会同时释放。线程通信在某些情况下,您会希望线程能够相互通信。如前所述,您可以通过创建Event对象来完成此操作。但更常见的方法是使用队列(Queue)。在我们的示例中,这两种方法都涉及。现在让我们看看它是什么样子:让我们把它分解并分析它。首先,我们有一个创建者函数(又名生产者),我们用它来创建我们想要操作(或消费)的数据。然后使用另一个函数my_consumer来处理刚刚创建的数据。Creator函数使用Queue的put方法向队列中插入数据,consumer会不断的检查是否有更多的数据,发现有数据就进行处理。Queue对象处理了所有获取和释放锁的过程,我们不太关心。在这个例子中,首先创建了一个列表,然后创建了两个线程,一个作为生产者,一个作为消费者。您会注意到我们将Queue对象传递给两个线程,这隐藏了有关锁处理的详细信息。队列实现数据从第一个线程到第二个线程的传输。第一个线程在往队列中放入数据的时候,也会传递一个Event事件,然后自己挂起,等待事件结束。在消费者端,也就是第二个线程,它做的是数据处理。当数据处理完成后,会调用Event事件的set方法,通知第一个线程数据已经处理完毕,可以继续生产了。最后一行代码调用Queue对象的join方法,该方法告诉Queue等待所有线程完成。当第一个线程将所有数据放入队列后,它就会结束运行。现在您知道了如何使用线程以及它们擅长什么,希望您会发现它们在您的代码中很有用。
