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
' % (nloop, ctime()),
    sleep(nsec)
    print 'loop%s done at: %s
' % (nloop, ctime()),
    lock.release()

def main():
    print 'starting at: %s
' % 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
' %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的使用,那时候,将不需要考虑这些锁的问题。

原文地址:https://www.cnblogs.com/SRL-Southern/p/5095386.html