Python 高级学习#1 多线程编程

本章将了解python的多线程机制和GIL,了解threading模块与thread模块的区别和关系,熟练掌握使用threading.Thread进行多线程执行。

什么是多线程?

引言

在多线程(Multithread)编程出现之前,电脑程序的运行由一个执行序列组成,执行序列按顺序在主机的CPU运行。即使子任务相互独立,互相无关时也是按照一条线的顺序执行。
所以多线程编程的目的就是并行的运行这些相互独立的子任务,大幅度的提高整个任务的效率。
多线程编程对于某些任务是非常理想的,这些任务通常具有以下特点:

  • 本质是异步的,需要有多个并发事物。
  • 各个事物的运行顺序可以是不确定的,随机的,不可预测的。
  • 这些子任务可能需要计算出一个中间结果,最终合并为最后的结果。
  • 通常,运算密集型的任务比较容易分隔成多个子任务,如果使用单线程处理,则要处理多个外部输入源就很难。

一个顺序执行的程序要从每个IO终端信道检查用户输入时,程序无论如何也不能在读取IO终端信道的时候阻塞。因为用户输入的到达是不确定的,阻塞会导致其他IO信息的数据不能被处理。所以顺序执行通常使用非阻塞IO,或带有计时器的阻塞IO。

进程与线程

计算机程序只不过是磁盘中可执行的二进制的数据,他们只有在被读取到内存中,被操作系统调用的时候才开始他们的生命周期。
进程使程序的一次执行。**每个进程都有自己独立的地址空间、内存、数据栈以及其他记录其运行轨迹的辅助数据。**操作系统管理在其上运行的所有进程,并为这些进程公平的分配时间。进程也可以通过fork和spawn来完成其他任务。不过各个进程之间相互独立,所以只能使用进程间通讯(IPC)的方式,而不能直接共享信息。

线程跟进程有些相似,不同的是,所有线程运行在同一个进程中,共享相同的运行环境。他们可以想象成是在进程中并行运行的迷你进程。线程有开始、顺序执行、结束三部分。它有一个自己的指令指针,记录自己运行到什么地方,线程的运行可能被抢占(中断),或者暂时的被挂起(也叫睡眠),让其他的线程运行,这叫做让步。

一个进程中的各个线程之间共享一片数据空间,所以线程之间可以更加方便的共享数据以及相互通信。
然而实际中,对于单核CPU,真正的并发是不可能的,每个线程会被安排成每次只运行一小会,然后就把CPU让出来(系统中断),让其他的线程运行。关于进程与线程更多的内容需要去了解《操作系统原理》。

Python中的多线程

全局解释器锁(GIL)

Python代码的执行由python虚拟机(解释器主循环)来控制。对于python虚拟机的访问由全局解释器锁(GIL)来控制,这个锁保证同一时刻只有一个线程在运行。
在调用外部代码(如C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止。对此感兴趣的可以查看Python/ceval.c文件。

当一个线程结束计算时,就算是退出。。线程可以调用thread.exit()或者使用python退出进程的标准方法,如sys.exit()结束线程。不过,你不可以直接kill一个线程。

在python中使用线程

通常有两种方式:

  • thread模块:通常不推荐使用,最大原因是当主线程退出时,其他线程没有被清楚就退出了。
  • threading模块:推荐使用,对线程支持更为完善。

这里先从thread模块开始学习。thread模块中,除了产生线程之外,thread模块也提供了基本的同步数据结构的锁对象。start_new_thread()函数是thread模块中的关键函数,其参数为:函数、函数的参数以及可选的关键字参数。这个函数会产生一个新线程来运行。
thread模块函数还有:

  • allocate_lock():分配一个LockType类型的所对象。
  • exit():让线程退出。

LockType类型对象方法:

  • acquire(wait=None):尝试获取所对象。
  • locked():如果获取了锁对象返回True,否则返回False。
  • release():释放锁。
# thread.start_new_thread()使用例子
import thread
from time import ctime,sleep

def loop0():
    print 'start loop 0 at: ',ctime()
    sleep(4)
    print 'loop 0 down at ',ctime()

def loop1():
    print 'start loop 1 at: ',ctime()
    sleep(2)
    print 'loop 1 down at ',ctime()

def main():
    print 'start at: ',ctime()
    thread.start_new_thread(loop0,())
    thread.start_new_thread(loop1,())
    sleep(6)
    print 'all done at: ',ctime()

if __name__ == '__main__':
    main()
# start at:  Tue Jan 21 15:31:13 2020
# start loop 0 at:  Tue Jan 21 15:31:13 2020
# start loop 1 at:  Tue Jan 21 15:31:13 2020
# loop 1 down at  Tue Jan 21 15:31:15 2020
# loop 0 down at  Tue Jan 21 15:31:17 2020
# all done at:  Tue Jan 21 15:31:19 2020

其中在主函数中用到了sleep(6),这是因为两个loop分别只用4秒与2秒,sleep在此处的作用是同步线程,如果没有让主线程停止下来,那么主线程会过早的退出,会连带退出子线程。

对于线程同步,使用sleep显然不是最好的办法,此处使用sleep的前提是已知两个线程的执行时间,而线程的大部分使用场景并不能满足这样的前提。此时就要使用多线程管理中的了。将之前的程序用锁改进后:

import thread
from time import ctime,sleep

loops=[4,2]

def loop(nloop,nsec,lock):
    print 'start loop ',nloop, 'at: ',ctime()
    sleep(nsec)
    print 'loop ',nloop, ' down at ',ctime()
    lock.release()

def main():
    print 'start at: ',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: ',ctime()

if __name__ == '__main__':
    main()

两段函数主要的区别在于main()函数中,通过三个for循环,我们先调用thread.allocate_lock()函数创建一个锁列表,并分别调用acquire()获得锁。第二个循环开始线程,第三个循环按照顺序检测锁是否被释放(release)。

threading模块

接下来来看更为高级的threading模块。threading模块对象:

  • Thread:表示一个线程的执行对象。
  • Lock:锁原语对象(跟thread模块里的锁对象相同)
  • RLock:可重入锁对象。使单线程可以再次获得已经获得了的锁。
  • Condition:条件变量对象能让一个线程停下来,等待其他线程满足了某个“条件”。如,状态的改变或值的改变。
  • Event:通用的条件变量。多个线程可以等待某个事件的放生,在事件发生后,所有线程都会被激活。
  • Semaphore:信号量,为等待锁的线程提供一个类似“等候室”的结构。
  • BoudedSemaphore:与Semaphore类似,只是它不允许超过初始值。
  • Timer:与Thread相似,只是要等待一段时间后才开始运行。

另一个不推荐使用thread模块的原因是,它不支持守护线程。当主线程退出时,所有子线程无论是否在工作,都会被强制退出。
threading模块支持守护线程(deamon):如果设定一个线程为守护线程,就表示这个线程是不重要的,在晋城退出时,不用等待这个线程退出,例如服务器线程通常就是运行在一个无限循环中。

threading的Thread类是主要的运行对象,有许多thread模块里没有的函数。Thread对象的函数:

  • start():开始线程执行。
  • run():定义线程的功能函数(一般会被子类重写)
  • join(timeout=None):程序挂起,直到线程结束;如果给了timeout,则最多阻塞timeout秒。
  • getName():返回线程名字。
  • setName(name):设置线程名字。
  • isAlive:布尔标志,表示这个线程是否还在运行中。
  • isDaemon():返回线程的daemon标志。
  • setDaemon():把线程的daemon标志设为daemonic(需要在调用start()前调用)。

现在将上面的例子,用threading模块进行重写:

import threading
from time import ctime,sleep

loops=[4,2]

def loop(nloop,nsec):
    print 'start loop ',nloop, 'at: ',ctime()
    sleep(nsec)
    print 'loop ',nloop, ' down at ',ctime()

def main():
    print 'start at: ',ctime()
    threads=[]
    nloops=range(len(loops))
    for i in nloops:
        t=threading.Thread(target=loop,args=(i,loops[i]))
        threads.append(t)

    for i in nloops:
        threads[i].start()

    for i in nloops:
        threads[i].join()

    print 'all done at: ',ctime()

if __name__ == '__main__':
    main()

与使用thread模块不同的是,threading可以将所有进程创建好后,再一起调用start()函数启动线程,而不是创建一个启动一个。而且不需要再管理锁(分配、获得、释放、检查锁),只需要简单的对每个线程调用join()函数就可以。

join()函数会等到线程结束,或在给了timeout参数的时候,等到超时(也叫spinlock)。

如果你的主线程除了线程结束外,还有其他事情要做,就不需要join,只有在需要等待线程结束才进行接下来的步骤时需要使用join()。

threading模块的其他函数:

  • activeCount():当前活动的线程对象的数量。
  • currentThread():返回当前线程对象。
  • enumerate():返回当前活动线程的列表。
  • settrace():为所有线程设置一个跟踪函数。
  • setprofile():为所有线程设置一个profile函数。

生产者-消费者问题和Queue模块

生产者生产货物,放入一个队列。消费者消耗生产者的货物。
生产货物所花费的时间和消耗货物的时间均不确定。

Queue模块函数:

  • queue(size):创建一个大小为size的Queue对象。

Queue对象函数:

  • qsize():返回队列的大小。
  • empty():如果队列为空返回True,否则False。
  • full():如果队列已满返回True,否则False。
  • put(item, block=0):把item放入队列,如果给了block,函数会一直阻塞到队列中有空间为止。
  • get(block=0):从队列中取出一个对象,block作用同上。

Queue模块可以用来进行线程间的通信,让各个线程共享数据。生产者-消费者代码:

# producer-consumer

from random import randint
from time import sleep
from Queue import Queue
import threading

# 创建对象放入队列
def writeQ(queue):
    print 'produce object for Q ...'
    queue.put('xxx', 1)
    print 'size now: ', queue.qsize()

# 从队列中取出一个对象,get中的block会在队列为空的时候阻塞函数
def readQ(queue):
    val=queue.get(1)
    print 'consume object from Q... size now: ', queue.qsize()

def writer(queue,loops):
    for i in range(loops):
        writeQ(queue)
        sleep(randint(1,3))

def reader(queue,loops):
    for i in range(loops):
        readQ(queue)
        sleep(randint(1,3))

funcs=[writer,reader]
nfuncs=range(len(funcs))

# 两个线程,一个运行生产者,一个运行消费者
def main():
    nloops=randint(2,5)
    q=Queue(32)

    threads=[]

    for i in nfuncs:
        t=threading.Thread(target=funcs[i], args=(q,nloops))
        threads.append(t)

    for i in nfuncs:
        threads[i].start()

    for i in nfuncs:
        threads[i].join()

    print 'all done'

if __name__ == '__main__':
    main()

你可能感兴趣的:(Python)