18.4 thread模块

我们先看看thread模块都提供了些什么。除了产生线程外,thread模块也提供了基本的同步数据结构锁对象(lock object,也叫原语锁、简单锁、互斥锁、互斥量、二值信号量)。如之前所说,同步原语与线程的管理是密不可分的。

表18.1中所列的是常用的线程函数以及LockType类型的锁对象的方法。

18.4 thread模块 - 图1

start_new_thread()函数是thread模块的一个关键函数,它的语法与内建的apply()函数完全一样,其参数为:函数,函数的参数以及可选的关键字参数。不同的是,函数不是在主线程里运行,而是产生一个新的线程来运行这个函数。

现在,把线程加入到我们的onethr.py例子中。稍微改变一下loop*()函数的调用方法,我们得到了例18.2的mtsleepl.py。

这儿执行的是和onethr.py中一样的循环,不同的是,这一次我们使用的是thread模块提供的简单的多线程的机制。两个循环并发地被执行(显然,短的那个先结束)。总的运行时间为最慢的那个线程的运行时间,而不是所有的线程的运行时间之和。

例18.2

18.4 thread模块 - 图2

Start_new_thread()要求一定要有前两个参数。所以,就算我们想要运行的函数不要参数,我们也要传一个空的元组。

这个程序的输出与之前的输出大不相同,之前是运行了6~7秒,而现在则是4秒,是最长的循环的运行时间与其他的代码的时间总和。

18.4 thread模块 - 图3

睡眠4秒和2秒的代码现在是并发执行的。这样,就使得总的运行时间被缩短了。可以看到,loop1甚至在loop()前面就结束了。程序的一大不同之处就是多了一个“sleep(6)”的函数调用。为什么要加上这一句呢?因为如果我们没有让主线程停下来,那主线程就会运行下一条语句,显示“all done”,然后就关闭运行着loop0()和loop1()的两个线程,退出了。

我们没有写让主线程停下来等所有子线程结束之后再继续运行的代码,这就是我们之前说线程需要同步的原因。在这里,我们使用了sleep()函数作为我们的同步机制。我们使用6秒是因为我们已经知道,两个线程(你知道,一个要4秒,一个要2秒)在主线程等待6秒后应该已经结束了。

你也许在想,应该有什么好的管理线程的方法,而不是在主线程里做一个额外的延时6秒的操作。因为这样一来,我们的总的运行时间并不比单线程的版本来得少。而且,像这样使用sleep()函数做线程的同步操作是不可靠的。如果我们的循环的执行时间不能事先确定的话,那怎么办呢?这可能造成主线程过早或过晚退出。这就是锁的用武之地了。

上一次修改程序,我们去掉了loop函数,现在,我们要再一次修改程序为例18.3的mtsleep2.py,引入锁的概念。运行它,我们看到,其输出与mtsleepl.py很相似,唯一的区别是我们不用为线程什么时候结束再做额外的等待。使用了锁,我们就可以在两个线程都退出后,马上退出。

18.4 thread模块 - 图4

我们是怎么通过锁来完成任务的呢?先看一看代码吧。

例18.3 使用线程和锁(mtsleep2.py)

这里,使用锁比mtsleepl.py那里在主线程中使用sleep()函数更合理。

18.4 thread模块 - 图5

逐行解释

1 ~ 6行

在Unix启动信息行后面,我们导入了thread模块和time模块里我们早已熟悉的几个函数。我们不再在函数里写死要等4秒和2秒,而是使用一个loop()函数,把这些常量放在一个列表loops里。

8 ~ 12行

loop()函数替换了我们之前的那几个loop*()函数。在loop()函数里,增加了一些锁的操作。一个很明显的改变是,我们现在要在函数中记录下循环的号码和要睡眠的时间。最后一个不一样的地方就是那个锁了。每个线程都会被分配一个事先已经获得的锁,在sleep()的时间到了之后就释放相应的锁以通知主线程,这个线程已经结束了。

14 ~ 34行

主要的工作在包含三个循环的main()函数中完成。我们先调用thread.allocate_lock()函数创建一个锁的列表,并分别调用各个锁的acquire()函数获得锁。获得锁表示“把锁锁上”。锁上后,我们就把锁放到锁列表locks中。下一个循环创建线程,每个线程都用各自的循环号,睡眠时间和锁为参数去调用loop()函数。为什么我们不在创建锁的循环里创建线程呢?有以下几个原因:(1)我们想到实现线程的同步,所以要让“所有的马同时冲出栅栏”。(2)获取锁要花一些时间,如果你的线程退出得“太快”,可能会导致还没有获得锁,线程就已经结束了的情况。

在线程结束的时候,线程要自己去做解锁操作。最后一个循环只是坐在那一直等(达到暂停主线程的目的),直到两个锁都被解锁为止才继续运行。由于我们顺序检查每一个锁,所以我们可能会要长时间地等待运行时间长且放在前面的线程,当这些线程的锁释放之后,后面的锁可能早就释放了(表示对应的线程已经运行完了)。结果主线程只能毫不停歇地完成对后面这些锁的检查。最后两行代码的意思你应该已经知道了,就是只有在我们直接运行这个脚本时,才运行main()函数。

在核心笔记中我们就已经说过,使用thread模块只是为了给读者演示如何进行多线程编程。你的多线程程序应该使用更高级别的模块,如threading等。现在我们就开始讨论它。