您的位置:首页 - 教程 - python - 正文
Python标准模块--threading

1 模块简介

threading模块在Python1.5.2中首次引入,是低级thread模块的一个增强版。threading模块让线程使用起来更加容易,允许程序同一时间运行多个操作。

不过请注意,Python中的线程最好是与IO操作一起工作,比如从网络上下载资源或者从你的电脑中读取文件和目录。如果你需要处理一些CPU密集的任务,你最好是看看Python的multiprocessing模块。原因就是Python有GIL锁(解释器全局锁),使得所有的线程在主线程内运行。由于这个原因,当你使用线程执行CPU密集型任务时,你可能会发现它会运行的很慢。下面,我们主要集中在IO操作--线程做的好的场景。

2 模块使用

2.1 线程入门

一个线程允许你像运行一个独立的程序一样,运行一段独立的代码,类似于subprocess,区别在于,线程运行的是函数或者类,而不是一个独立的程序。我发现使用一个具体的实例会有助于我们更加理解概念。实例如下,

import threading

def doubler(number):
    print(threading.currentThread().getName() + "\n")
    print(number * 2)
    print""

if __name__ == "__main__":
    for i in range(5):
        my_thread = threading.Thread(target = doubler,args = (i,))
        my_thread.start()
         my_thread.join()

我们首先引入threading模块,创建一个常规的函数doubler。这个函数将输入值乘以2,它也打印出调用这个函数的线程的名字,最后打印一个空白行。你也许会注意到当我们实例化一个线程时,我们设置它的target为我们的doubler函数,然后我们将变量传递给这个函数。使用args参数看起来有些奇怪,是因为我们需要向doubler函数传递一个序列,但是它只介绍一个变量,我们我们需要在末尾放入一个逗号,从而构造出一个序列。

如果你想等待一个线程结束,你需要调用它的join()方法。

当你运行这段代码时,你应该可以得到如下的结果,

Thread-1

0

Thread-2

2

Thread-3

4

Thread-4

6

Thread-5

8

一般情况下,你并不希望将输出打印到标准输出上,当你这样做的时候,会导致乱七八糟的混乱。你应该使用Python的logging模块。这篇文章具体会告诉你如何使用logging模块,Python标准模块--logging。logging模块是线程安全的,性能也很优越。让我们把上面的代码修改一下,加上logging模块,如下所示,

import threading
import logging

def get_logger():
    logger = logging.getLogger("threading_example")
    logger.setLevel(logging.DEBUG)

    fh = logging.FileHandler("threading.log")
    fmt = "%(asctime)s - %(threadName)s - %(levelname)s - %(message)s"
    formatter = logging.Formatter(fmt)
    fh.setFormatter(formatter)

    logger.addHandler(fh)
    return logger

def doubler(number,logger):
    logger.debug('double function executing')
    result = number * 2
    logger.debug("double function ended with {}".format(result))

if __name__ == "__main__":
    logger = get_logger()
    thread_names = ["Mike","George","Wanda","Dingbat","Nina"]
    for i in range(5):
        my_thread = threading.Thread(target = doubler,name = thread_names[i],args = (i,logger))
        my_thread.start()

这段代码最大的改动就是加入了get_logger函数。这段代码创建一个级别为debug的logger。它将会把日志保存在当前的工作目录(例如,这个脚本所运行的目录),日志文件名为threading.log,然后我们设置每行日志的格式。这个日志格式包括时间戳、线程名字、日志等级和要打印的消息。

在doubler函数中,我们将print语句修改为logging语句。你将会注意到当我们创建线程时,我们将logger传入到doubler函数中。我们这么做的原因是当你在每个线程中实例化logging对象时,你将会得到多个日志记录单例,你的日志中将会有很多重复的行。

最后,我们通过创建一个名称列表用于给创建的线程进行命名,通过使用name这个参数,给每个线程设置一个指定的名称。当你运行这段代码时,你就会在日志文件中得到如下内容,

2016-11-11 14:34:35,350 - Mike - DEBUG - double function executing
2016-11-11 14:34:35,350 - Mike - DEBUG - double function ended with 0
2016-11-11 14:34:35,350 - George - DEBUG - double function executing
2016-11-11 14:34:35,350 - Wanda - DEBUG - double function executing
2016-11-11 14:34:35,351 - George - DEBUG - double function ended with 2
2016-11-11 14:34:35,351 - Wanda - DEBUG - double function ended with 4
2016-11-11 14:34:35,351 - Dingbat - DEBUG - double function executing
2016-11-11 14:34:35,351 - Dingbat - DEBUG - double function ended with 6
2016-11-11 14:34:35,351 - Nina - DEBUG - double function executing
2016-11-11 14:34:35,351 - Nina - DEBUG - double function ended with 8

输出具有很好的可解释性。对于这部分,我想挖掘出更多的主题也就是threading.Thread。让我们来看最后一个例子,不再直接调用Thread,我们创建我们的子类。

import threading
import logging

class MyThread(threading.Thread):
    def __init__(self,number,logger):
        threading.Thread.__init__(self)
        self.number = number
        self.logger = logger

    def run(self):
        logger.debug("Calling doubler")
        doubler(self.number,self.logger)

def get_logger():
    logger = logging.getLogger("threading_example")
    logger.setLevel(logging.DEBUG)

    fh = logging.FileHandler("threading_class.log")
    fmt = "%(asctime)s - %(threadName)s - %(levelname)s - %(message)s"
    formatter = logging.Formatter(fmt)
    fh.setFormatter(formatter)

    logger.addHandler(fh)
    return logger

def doubler(number,logger):
    logger.debug('double function executing')
    result = number * 2
    logger.debug("double function ended with {}".format(result))

if __name__ == "__main__":
    logger = get_logger()
    thread_names = ["Mike","George","Wanda","Dingbat","Nina"]
    for i in range(5):
        thread = MyThread(i,logger)
        thread.setName(thread_names[i])
        thread.start()

在这个例子中,我们通过threading.Thread创建子类。我们传入想翻倍的数字number和logging对象。但是这次,我们通过另一种方式--调用线程对象的setName方法来设置线程名字。当你调用start,它将会通过调用run方法来运行你所定义的线程。在我们的类例,我们调用doubler函数来完成我们所需的处理。输出和之前的输出很相似,只是我们多加了一行调用日志。尝试着运行这段代码,看看你得到的是什么?

2.2 线程锁和同步

当你创建了多个线程时,你可能会发现你需要考虑如何避免冲突。我的意思就是你可能会遇到多个线程在同一时间需要访问同一个资源。如果你不考虑这个问题,你将会遇到一些发生在最坏的时刻并且通常是在生产环境下的问题。

解决方案就是使用线程锁。Python的threading模块提供了线程锁,线程锁被一个线程或者没有线程所拥有。一个线程尝试着获取资源上已经被锁的线程锁,那个线程会被中止,直到线程锁被释放。让我们来看看一个没有采用任何锁机制的实例,

import threading

total = 0

def update_total(amount):
    global total
    total += amount
    print (total)

if __name__ == "__main__":
    for i in range(10):
        my_thread = threading.Thread(target = update_total,args = (5,))
        my_thread.start()

可以调用time.sleep函数让这段程序更加有意思。这个问题主要还是一个线程调用update_total,在它完成更新之前,另一个线程也可能会去调用它并尝试着去更新。依赖于操作的顺序,这个值可能立刻就会被相加。

让我们在这个函数中增加一个线程锁,有两种方式去实现这个,一种就是使用try/finally,正如我们所希望,线程锁常常出于释放状态。下面就是例子,

import threading

total = 0
lock = threading.Lock()

def update_total(amount):
    global total
    lock.acquire()
    try:
        total += amount
    finally:
        lock.release()
    print ("total = " + str(total) + '\n')

if __name__ == "__main__":
    for i in range(10):
        my_thread = threading.Thread(target = update_total,args = (5,))
        my_thread.start()

这里,我们在我们处理任何任务之前,先获取线程锁。然后,我们尝试着更新total的值,释放线程锁并打印出total当前的值。我们还可以使用Python的with语句来做类似的事情。

import threading

total = 0
lock = threading.Lock()

def update_total(amount):
    global total
    with lock:
        total += amount
    print ("total = " + str(total) + '\n')

if __name__ == "__main__":
    for i in range(10):
        my_thread = threading.Thread(target = update_total,args = (5,))
        my_thread.start()

正如你所看到的,我们不再需要try/finally作为上下文管理器,而是通过with语句完成了这些工作。

有时候,你也需要多个线程获取多个函数,当你刚开始写代码时,你可能处理一些任务,如下,

import threading

total = 0
lock = threading.Lock()

def do_something():
    lock.acquire()
    try:
        print("Lock acquired in the do_something function")
    finally:
        lock.release()
        print("Lock released in the do_something function")
    return "Done doing something"

def do_something_else():
    lock.acquire()
    try:
        print("Lock acquired in the do_something_else function")
    finally:
        lock.release()
        print("Lock released in the do_something_else function")
    return "Finished something else"

if __name__ == "__main__":
    result_one = do_something()
    result_two = do_something_else()

这个或许在当前环境下运行没有问题,但是假设你右多个线程调用这两个函数。当一个线程在运行这个函数,另一个线程可能也会修改数据,最终你所得到将会是不正确的结果。而你也许并没有立刻意识到结果是错误的。有没有什么解决方案呢?让我们仔细揣摩揣摩。

第一个常见的想法就是在这两个函数之前增加一个线程锁,让我们将以上的代码修改如下,

import threading

total = 0
lock = threading.Lock()

def do_something():
    lock.acquire()
    try:
        print("Lock acquired in the do_something function")
    finally:
        lock.release()
        print("Lock released in the do_something function")
    return "Done doing something"

def do_something_else():
    lock.acquire()
    try:
        print("Lock acquired in the do_something_else function")
    finally:
        lock.release()
        print("Lock released in the do_something_else function")
    return "Finished something else"

def main():
    with lock:
        result_one = do_something()
        result_two = do_something_else()
    print(result_one)
    print(result_two)

if __name__ == "__main__":
    main()

当你实际运行这段代码时,你会发现它出于悬挂状态而无法终止。原因就是当我们告诉threading模块去获取线程锁时,我们调用第一个函数,它会发现线程锁已经保持和阻塞,它将会继续保持直到线程锁被释放,但是这种情况永远不会发生。

解决方法就是使用Re-Entrant Lock。Python的threading模块通过RLock函数提供了这个功能。将lock = threading.Lock()改为lock = threading.RLock(),再运行一下,你的代码就会正常运行了。

如果你尝试在实际的线程中运行以上的代码,我们可以如下方式调用main函数。

if __name__ == "__main__":
    for i in range(10):
        my_thread = threading.Thread(target = main)
        my_thread.start()

这段代码将会在每个线程中运行main函数,main函数又会调用另外两个函数,你将会得到10组输出。

2.3 定时器

threading模块有一个精巧的类--Timer,你可以用它表示一个在指定时间之后才发生的操作。我们可以像常规的线程那样,通过使用start()方法启动我们的定时器,也可以使用定时器的cancel()方法来终止一个定时器。你应该了解到,你可以在定时器启动之前终止它。

我遇到这样一个场景,我需要与启动的子进程进行通信,但是我还需要它在超时时会终止。有很多种方法来解决这个问题,我喜欢的解决方法是使用threading模块的Timer类。

我们使用ping这个命令,在Linux中,ping命令会一直运行,直到你杀死它。所以Timer类在Linux平台上就非常有用。示例如下,

import subprocess

from threading import Timer

kill = lambda process:process.kill()

cmd = ["ping","www.baidu.com"]

ping = subprocess.Popen(cmd,stdout = subprocess.PIPE,stderr = subprocess.PIPE)

my_timer = Timer(5,kill,[ping])

try:
    my_timer.start()
    stdout,stderr = ping.communicate()
finally:
    my_timer.cancel()

print(str(stdout))

这里,我们首先定义一个lambda函数用于杀死进程。然后我们开始ping,并且创建一个Timer对象。你将会注意到第一个变量是等待时间,然后调用函数和传入相应的变量。在这个示例中,我们的函数就是一个lambda函数,并且我们传入一个变量列表,这个列表只有一个元素。如果你运行这段代码,它会运行5秒钟,然后打印出ping的结果。

2.4 其它的线程组件

threading模块还支持其它组件。例如,你可以创建一个Semaphore--计算机科学中最经典的同步原语之一。一个Semaphore管理一个内部的计数器,当你调用acquire方法,它会递减;如果你调用release,它就会递增。计数器不能低于0。当它为0时,你碰巧调用了acquire,它就会阻塞。

另一个有用的工具就是使用Event。它会允许你在线程之间通过信号通信。我们将会在下一部分展示使用Event的示例。

最后,Python3.2中也有Barrier对象。Barrier管理一个线程池,线程池中的线程需要等待其它线程执行完毕。为了通过屏障,需要调用wait()方法,这个方法可以阻塞直到所有的线程已经被调用。然后,它就会同时释放所有的线程。

2.5 线程通信

你可能想让线程之间互相通信。正如我们在上一部分提到的,你可以使用Event,但是更为通用的方法是使用Queue。在我们这个示例中,我们实际上都使用了。让我们来看看吧,

import threading

from queue import Queue

def creator(data,q):
    print("Creating data and putting it on the queue")
    for item in data:
        evt = threading.Event()
        q.put((item,evt))
        print("Wait for data to be doubled")
        evt.wait()

def my_consumer(q):
    while True:
        data,evt = q.get()
        print("data found to be process:{}".format(data))
        processed = data * 2
        print(processed)
        evt.set()
        q.task_done()

if __name__ == "__main__":
    q = Queue()
    data = [5,10,13,-1]
    thread_one = threading.Thread(target = creator,args = (data,q))
    thread_two = threading.Thread(target = my_consumer,args = (q,))
    thread_one.start()
    thread_two.start()
    q.join()

首先,我们有一个creator(AKA,一个生产者)函数用于创建需要处理的数据。我们还有另一个函数用于处理待处理的数据,我们命名为my_consumer。生产者函数使用Queue的put方法将数据放入Queue中,消费者函数会一直检查数据,当可以获得数据,就会处理它。Queue会自己处理所有的获取和释放,所以你不需要关注。

在这个例子中,我们首先创建一个用于数值翻倍的列表。然后我们创建两个线程,一个用于生产者,另一个用于消费者。你将会注意到我们给每个线程都传入一个Queue对象,每个线程的背后都会有线程所是如何处理的。队列会将第一个线程的返回数据传入给第二个线程。当第一个线程将数据放入到队列中,它也传入了一个Event对象,然后等待这个事件的结束。在消费者函数中,数据被处理,当它把数据处理完毕,它会调用事件的set方法,这个将会告诉第一个线程,第二个线程已经处理完毕数据,第一个线程可以继续发送数据。

最后一行代码调用了Queue对象的join方法,这会告诉Queue去等待所有的线程都执行结束。当第一个线程把所有的元素都放入Queue中之后,它就会结束。

2.6 总结

在本文中,我们讲解了很多知识,你可以学习到如下知识:

  • 基本的线程
  • 线程锁如何工作
  • 什么是事件,以及如何使用
  • 如何使用定时器
  • 通过Queues/Events,线程之间如何通信

现在,你已经了解到线程是如何使用的,以及它们是如何工作的,我希望你可以在代码中找到更多关于线程使用的技巧。

3 Reference

Python 201


评论: