《Python核心编程》多线程编程

线程和Python

全局解释锁

Python代码的执行是由Python虚拟机进行控制的。Python在设计时是这样考虑的,在主循环中同时只能有一个控制线程在执行,就像单核CPU系统中的多线程一样。内存中可以有许多程序,但是在任意给定时刻只能有一个程序在运行。同理,尽管Python解释器中可以运行多个线程,但是在任意给定时刻只有一个线程会被解释器执行。

对Python虚拟机的访问是由全局解释锁(GIL)控制的。这个锁就是用来保证同时只能有一个线程运行的。

在多线程环境中,Python虚拟机将按照下面所述的方式执行。

  1. 设置GIL
  2. 切换进一个线程去运行
  3. 执行下面操作之一 (a.指定数量的字节码指令   b.线程主动让出控制权)
  4. 切换出线程
  5. 解锁GIL
  6. 重复上述步骤

当调用外部代码时,GIL会保持锁定,直至函数执行结束(因为在这期间没有Python字节码计数??)

不适用线程的情况

#该脚本在一个单线程程序里连续执行两个循环。一个循环必须在另一个开始前完成。总共消耗的时间是每个虚幻所用时间之和

from time import sleep,ctime

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

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

def main():
    print('staring at :',ctime())
    loop0()
    loop1()
    print('all Done at :',ctime())

if __name__=='__main__':
    main()

使用thread模板

#这里执行的循环和上面一样,这次使用了多线程机制,两个循环是并发执行的。总的运行时间由慢的决定
import _thread
from time import sleep,ctime

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

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

def main():
    print('staring at :',ctime())
    _thread.start_new_thread(loop0,())
    _thread.start_new_thread(loop1,())
    sleep(6) #如果没有这条语句,则会直接执行all Done语句,然后退出,loop0()和loop1()这两个线程也会直接中止
    print('all Done at :',ctime())

if __name__=='__main__':
    main()

改进sleep(6),引入锁

引入锁之后,我们不需要再像mtsleepA那样等待额外的时间后才结束。通过使用锁,我们可以在所有线程全部完成执行后立即退出

import _thread
from time import sleep,ctime

loops = [4,2]
def loop(nloop,nsec,lock):
    print('start loop ',nloop,'at:',ctime())
    sleep(nsec)
    print('loop',nloop,' done at:',ctime())
    lock.release()

def main():
    print ('staring 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()



threading模块

现在介绍级别更高的threading模块。

核心提示:守护线程

避免使用thread模块的另一个原因是该模块不支持守护线程这个概念,当主线程退出时,所有子线程都将中止,不管它们是否在工作。

threading模块支持守护线程,其工作方式是:守护线程一般是一个等待客户端请求服务的服务器。如果没有客户端请求,守护线程就是空闲的。如果把一个线程设置为守护线程,就表示这个线程是不重要的,进程退出时不需要等待这个线程执行完成。

如果主线程准备退出时,不需要等待某些子线程完成,就可以为这些子线程设置守护线程标记,该标记值为真时,表示该线程是不重要的,或者说该线程只是用来等待客户端请求而不做任何事情。

要将一个线程设置为守护线程,需要在启动线程之前执行如下赋值语句:thread.daemon=True.一个新的子线程会继承父线程的守护标记。

使用Thread类,可以有很多方法来创建线程。我们将介绍其中比较相似的三种方案(更倾向最后一种)

  • 创建Thread的实例,传给它一个函数。
  • 创建Thread的实例,传给它一个可调用的类实例。
  • 派生Thread的子类,并创建子类的实例。

创建Thread的实例,传给它一个函数
在第一个例子中,我们只是把Thread类实例化,然后将函数(及其参数)传递进去,和之前例子中采用的方式一样。

import threading
from time import sleep,ctime

loops = [4,2]

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

def main():
    print('starting 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(调用Thread())和调用thread.start_new_thread()的最大区别是新线程不会立即开始执行。这是一个非常有用的同步功能,尤其是当你并不希望线程立即开始执行时。

当所有线程都分配完成之后,通过调用每个线程的start()方法让它们开始执行,而不是在这之前就会执行。

相比于管理一组锁(分配、获取、释放、检查锁状态等)而言,这里只需要为每个线程调用join()方法即可。join()方法将等待线程结束,或者在提供了超时时间的情况下,达到超时时间。使用join()方法要比等待锁释放的无限循环更加清晰(这也是这种锁又称为自旋锁的原因)。

对于join()方法而言,其另一个重要方面是其实它根本不需要调用。一旦线程启动,它们就会一直执行,直到给定的函数完成后退出。如果主线程还有其他事情要去做,而不是等待这些线程完成(例如其他处理或者等待新的客户端请求),就可以不调用join()。join()方法只有在你需要等待线程完成的时候才是有用的。
派生Thread的子类,并创建子类的实例
 

#派生Thread的子类,并创建子类的实例
#本例中将对Thread子类化,而不是直接对其实例化。这将使我们在定制线程对象时拥有更多的灵活性,也能够简化线程创建的调用过程。

import threading
from time import sleep,ctime

loops = (4,2)

class MyThread(threading.Thread):
    def __init__(self,func,args,name=''):
        threading.Thread.__init__(self)
        self.name = name
        self.func = func
        self.args = args

    def run(self):
        self.func(*self.args)

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

def main():
    print ('starting at:', ctime ())
    threads = []
    nloops = range (len (loops))

    for i in nloops:
        t = MyThread(loop,(i,loops[i]),loop.__name__)
        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()

现在,对MyThread类进行修改,增加一些调试信息的输出,并将其存储为一个名为myThread的独立模块,以便在接下来的例子中导入这个类。除了简单地调用函数外,还将把结果保存在实例属性self.res中,并创建一个新的方法getResult()来获取这个值。
 

#派生Thread的子类,并创建子类的实例
import threading
from time import sleep,ctime

loops = (4,2)

class MyThread(threading.Thread):
    def __init__(self,func,args,name=''):
        threading.Thread.__init__(self)
        self.name = name
        self.func = func
        self.args = args

    def getResult(self):
        return self.res

    def run(self):
        print ('start loop ', self.name, 'at:', ctime ())
        self.res = self.func(*self.args)
        print (self.name, ' finished at:', ctime ())

同步原语

同步。一般在多线程代码中,总会有一些特定的函数或代码块不希望被多个线程同时执行,通常包括修改数据库,更新文件或其他会产生竞态条件的类似情况。下面使用两种类型的同步原语演示几个示例程序:锁/互斥,信号量。

锁是所有机制中最简单,最低级的机制,而信号量用于多线程竞争有限资源的情况。

锁示例

当多线程争夺锁时,允许第一个获得锁的线程进入临界区,并执行代码。所有之后到达的线程将被阻塞,直到第一个线程执行结束,退出临界区,并释放锁。此时,其他等待的线程可以获得锁并进入临界区。不过请记住,那些被阻塞的线程是没有顺序的(即不是先到先执行),胜出线程的选择是不确定的,而且还会根据Python实现的不同而有所区别。

#演示了锁和一些其他线程工具的使用
from atexit import register
from random import randrange
from threading import Thread,Lock,currentThread
from time import sleep,ctime
from __future__ import with_statement

class CleanOutputSet(set):
    def __str__(self):
        return ','.join(x for x in self)

lock = Lock()
#随机数量的线程(3~6个线程),每个线程暂停或睡眠2~4秒
loops = (randrange(2,5) for x in range(randrange(3,7)))
remaining = CleanOutputSet()

def loop(nsec):
    myname = currentThread().name
    with lock:
        remaining.add(myname)
        print('[%s] Started %s' % (ctime(),myname))
    sleep(nsec)
    with lock:
        remaining.remove(myname)
        print('[%s] Completed %s (%d secs)' % (ctime(),myname,nsec))
        print('(remaining:%s) '% (remaining or 'None'))


def _main():
    for pause in loops:
        print('****',pause,'-',loops,'-')
        Thread(target=loop,args=(pause,)).start()
#使用atexit.register()来注册_atexit()函数,以便让解释器在脚本退出前执行该函数
@register
def _atexit():
    print('all DONE at:',ctime())

if __name__ == '__main__':
    _main()

可是出现混乱的情况

I/O和访问相同的数据结构都属于临界区,因此需要用锁来防止多个线程同时进入临界区

#演示了锁和一些其他线程工具的使用
from atexit import register
from random import randrange
from threading import Thread,Lock,currentThread
from time import sleep,ctime
lock = Lock()
class CleanOutputSet(set):
    def __str__(self):
        return ','.join(x for x in self)
#随机数量的线程(3~6个线程),每个线程暂停或睡眠2~4秒
loops = (randrange(2,5) for x in range(randrange(3,7)))
remaining = CleanOutputSet()

def loop(nsec):
    myname = currentThread().name
    lock.acquire()
    remaining.add(myname)
    print('[%s] Started %s' % (ctime(),myname))
#3.x   print('[{0}] Started {1}'.format(ctime(),myname))
    lock.release()
    sleep(nsec)
    lock.acquire()
    remaining.remove(myname)
    print('[%s] Completed %s (%d secs)' % (ctime(),myname,nsec))
    print('(remaining:%s) '% (remaining or 'None'))
    lock.release()

def _main():
    for pause in loops:
        print('****',pause,'-',loops,'-')
        Thread(target=loop,args=(pause,)).start()
#使用atexit.register()来注册_atexit()函数,以便让解释器在脚本退出前执行该函数
@register
def _atexit():
    print('all DONE at:',ctime())

if __name__ == '__main__':
    _main()

信号量示例

信号量是最古老的同步原语之一。它是一个计数器,当资源消耗时递减,当资源释放时递增。你可以认为信号量代表它们的资源可用或不可用。消耗资源使计数器递减的操作习惯上称为P(),也称为wait,try,acquire,pend或procure。相对地,当一个线程对一个资源完成操作时,该资源需要返回资源池中。这个操作一般称为V(),也称为signal,increment,release,post,vacate。Python简化了所有的命名,使用和锁的函数/方法一样的名字:acquire和release。信号量比锁更加灵活,因为可以有多个线程,每个线程拥有有限资源的一个实例。

在下面的例子中,我们将模拟一个简化的糖果机。这个特制的机器只有5个可用的槽来保持库存(糖果)。如果所有的槽都满了,糖果就不能再加到这个机器中了;相似地,如果每个槽都空了,想要购买的消费者就无法买到糖果了。我们可以使用信号量来跟踪这些有限的资源(糖果槽)。

#该脚本使用了锁和信号量来模拟一个糖果机

from atexit import register
from random import randrange
from threading import BoundedSemaphore,Lock,Thread
from time import sleep,ctime

lock = Lock()
MAX = 5
candytray = BoundedSemaphore(MAX)
#添加糖果
def refill():
    lock.acquire()
    print('Refilling candy...')
    try:
        candytray.release()
    except ValueError:
        print('full,skipping')
    else:
        print('OK')
    lock.release()
#允许消费者获取一个单位的库存
def buy():
    lock.acquire()
    print('Buying candy...')
    if candytray.acquire(False): #检测是否所有资源都已经消费完
        print('OK')
    else:
        print('empty,skipping')
    lock.release()

def producer(loops):
    for i in range(loops):
        refill()
        sleep(randrange(3))

def consumer(loops):
    for i in range(loops):
        buy()
        sleep(randrange(3))

def _main():
    print('starting at:',ctime())
    nloops = randrange(2,6)
    print('THE CANDY MACHINE (full with %d bars)!' % MAX)
    Thread(target=consumer,args=(randrange(nloops,nloops+MAX+2),)).start() #buyer
    Thread(target=producer,args=(nloops,)).start()


@register
def _atexit():
    print('all DONE at:',ctime())

if __name__ == '__main__':
    _main()

threading模块包括两种信号量类:Semaphore和BoundedSemaphore。如你所知,信号量实际上就是计数器,它们从固定数量的有限资源起始

当分配一个单位的资源时,计数器值减1,而当一个单位的资源返回资源池时,计数器值加1。BoundedSemaphore的一个额外功能是这个计数器的值永远不会超过它的初始值,换句话说,它可以防范其中信号量释放次数多于获得次数的异常用例。

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

在这个场景下,商品或服务的生产者生产商品,然后将其放到类似队列的数据结构中。生产商品的时间是不确定的,同样消费者消费生产者生产的商品的时间也是不确定的。
 

#该生产者-消费者问题的实现使用了Queue对象,以及随机生产(消费)的商品的数量。生产者和消费者独立且并发地执行线程

from random import randint #使生产和消费地数量有所不同
from time import sleep
import queue
from myThread import MyThread

def writeQ(queue):
    print('producing object for Q...')
    queue.put('xxx',1)
    print('size now',queue.qsize())

def readQ(queue):
    val = queue.get(1)
    print('consumed 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(2,5))

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

def main():
    nloops = randint(2,5)
    q = queue.Queue(32)

    threads = []
    for i in nfuncs:
        t = MyThread(funcs[i],(q,nloops),funcs[i].__name__)
        threads.append(t)


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

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

    print('all done')

if __name__ == '__main__':
    main()

猜你喜欢

转载自blog.csdn.net/Mai_Dreizehn/article/details/86569260