2016/1/2 Python中的多线程(1):线程初探
---恢复内容开始---
新年第一篇,继续Python。
先来简单介绍线程和进程。
计算机刚开始发展的时候,程序都是从头到尾独占式地使用所有的内存和硬件资源,每个计算机只能同时跑一个程序。后来引进了一些机制来改进这种调用方法,包括流水线,多进程。我们开始并发执行程序,每个程序交替地被处理器调用,再极高的频率下,你会认为这些程序是在同时执行的,这也就是并发技术。用操作系统来管理并发,将程序读到内存中,然后被操作系统调用开始,它的生命周期就开始了。而每个程序的执行,都是用进程的方式执行,每个进程有自己的地址空间、内存、数据栈及别的什么东西。对于任何一个以进程方式执行的程序,都好似独占了全部的硬件资源。每个进程都有从地址0开始的虚拟内存地址。也就是说,进程是一定意义的抽象。然后操作系统管理所有的进程,给它们分配分时运行的时间。进程之间通过一定的进程间交互手段来通信,不能直接共享信息。
那什么是线程?
线程常常被称为轻量级进程,跟进程有些相似,不同的是所有的线程都运行在同一个进程中,共享同样的运行环境,有同样的地址空间、数据栈空间。可以认为在一个进程里,有很多并行执行的线程。
线程有开始,顺序执行和结束三个部分。它有一个自己的指令指针,记录自己运行到什么地方。线程的运行可能被抢占或者暂时挂起,让其他线程运行,这种方式叫做让步。
线程间由于共享了同样的数据空间,所以可以很方便地共享数据和通讯,但是,有一个问题是多个线程同时访问同一片数据,由于顺序不同,可能数据结果会不一致,产生了所谓的竞态条件。
总的来说,线程是在一个程序里设置的几个并发运行的过程,让几件事情可以同时执行。
Python中使用线程
Python的代码由Python虚拟机控制,可以通过全局解释器锁GIL来控制,也可以直接用一些模块来实现我们的需求。接下来主要来介绍thread和threading模块。thread模块一般不推荐使用,因为它在主线程退出时,其他线程若没有结束,还没有清除就退出了,而threading模块确保所有的重要的子线程都退出后才会结束进程。
默认情况下,Python对线程的支持是打开的。在交互模式下尝试导入thread模块没有错误就表示可用。
>>> import thread >>>
如果出现了导入错误,那么应该重新编译Python解释器才能运行,这里不作说明。
先来看一个没有多线程支持例子:
这里用了time模块的sleep()函数,里面输入一个浮点的参数,表示睡眠的秒数,意味着程序将被挂起这段时间。
from time import sleep, ctime def loop0(): print 'start loop 0 at: %s' % ctime() sleep(4) print 'loop 0 done at: %s' % ctime() def loop1(): print 'start loop 1 at: %s' % ctime() sleep(2) print 'loop 1 done at: %s' % ctime() def main(): print 'starting at: %s' % ctime() loop0() loop1() print 'all Done at: %s' % ctime() if __name__ == '__main__': main()
>>> starting at: Sat Jan 02 21:17:48 2016 start loop 0 at: Sat Jan 02 21:17:48 2016 loop 0 done at: Sat Jan 02 21:17:52 2016 start loop 1 at: Sat Jan 02 21:17:52 2016 loop 1 done at: Sat Jan 02 21:17:54 2016 all Done at: Sat Jan 02 21:17:54 2016
可以看到,程序毫无疑问的顺序执行了,但是,我们用sleep()挂起的时间并没有意义了。
所以,让我们看一下用了线程之后的方法:
import thread from time import sleep, ctime def loop0(): print 'start loop 0 at: %s' % ctime() sleep(4) print 'loop 0 done at: %s' % ctime() def loop1(): print 'start loop 1 at: %s' % ctime() sleep(2) print 'loop 1 done at: %s' % ctime() def main(): print 'starting at: %s' % ctime() thread.start_new_thread(loop0,()) thread.start_new_thread(loop1,()) sleep(6) print 'all Done at: %s'% ctime() if __name__ == '__main__': main()
结果会是这样的
>>> starting at: Sat Jan 02 21:23:58 2016 start loop 0 at: Sat Jan 02 21:23:58 2016 start loop 1 at: Sat Jan 02 21:23:58 2016 loop 1 done at: Sat Jan 02 21:24:00 2016 loop 0 done at: Sat Jan 02 21:24:02 2016 all Done at: Sat Jan 02 21:24:04 2016
可以看到,这一次loop1和loop0在程序开始后4秒就结束了,只是我们多了一句sleep(6),让整个程序最后还是跑了6秒,加这一句,是防止主线程结束后子线程也就退出了,导致根本没有执行完,但是这种方法实在是很愚蠢,我们最后程序还是跑了6秒才能结束,如果我们有一次并不知道子进程什么时候结束,比如说从键盘读到一条命令后结束,那么该如何写这样的语句呢。接下来我会介绍这种方法,我们先来看看thread这个模块在此处干了什么。
我们调用了thread的一个方法start_new_thread(funciton, args kwargs=None),这个方法的作用是产生一个新线程,在新线程中用指定的参数和可选的kwargs来调用这个函数。这是一个很简单的线程机制。
接下来用锁来杜绝在主线程中使用sleep()函数:
import thread from time import ctime, sleep loops = [4, 2] def loop(nloop, nsec, lock): print 'start loop%s at: %s\n' % (nloop, ctime()), sleep(nsec) print 'loop%s done at: %s\n' % (nloop, ctime()), lock.release() def main(): print 'starting at: %s\n' % ctime(), locks = [] nloops = range(len(loops)) for i in nloops: lock = thread.allocate_lock() lock.acquire() locks.append(lock) for i in nloops: thread.start_new_thread(loop, (i, loops[i], locks[i])) for i in nloops: while locks[i].locked(): pass print 'all DONE at: %s\n' %ctime(), if __name__ == '__main__': main()
显示结果是这样的:
>>> starting at: Sat Jan 02 21:55:30 2016 start loop0 at: Sat Jan 02 21:55:30 2016 start loop1 at: Sat Jan 02 21:55:30 2016 loop1 done at: Sat Jan 02 21:55:32 2016 loop0 done at: Sat Jan 02 21:55:34 2016 all DONE at: Sat Jan 02 21:55:34 2016
大家可以看到我用了一种很抽风的方式使用print语句,至于为什么不用基本的方法,各位可以看一下用原来的方法输出结果是怎样的。
这里面,我们用了用了thread.allocate_lock()函数来创建一个锁对象,然后将它存到一个锁的列表里去,每次都得调用acquire()函数来获得锁,也就是把锁锁上,锁上后,通过一个锁的列表,在循环里,每个线程分配到自己的锁,然后一起执行。在线程结束的时候,我们需要解锁。
为什么不在创建锁的过程中创建进程呢?有两个原因,一个是我们希望每个线程都是同步开始的,要让它们几乎同时开始。另一个是每次获取锁会花一定的时间,如果线程退出的太快,可能锁还没有获取,线程就结束了。
所以我们需要分配锁,获得锁,释放锁,来实现进程的同步。
今天先写到这里,下一次再说明threading的使用,那时候,将不需要考虑这些锁的问题。