Python进阶学习笔记之——多线程编程

多线程编程

1、Python 的多线程支持模块简介

Python 提供了多个模块来支持多线程编程,包括 thread、threading 和 Queue 模块等。程序是可以使用 thread 和 threading 模块来创建与管理线程。thread 模块提供了基本的线程和锁定支持而 threading 模块提供了更高级别、功能更全面的线程管理使用 Queue 模块,用户可以创建一支队列数据结构,用于在多线程之间进行共享

  • 核心提示:避免使用 thread 模块

    推荐使用更高级别的 threading 模块,而不使用 thread 模块有很多原因:

    1. threading 模块更加先进,有更好的线程支持,并且 thread 模块中的一些属性会和 threading 模块有冲突。
    2. 另一个原因是低级别的 thread 模块拥有的同步原语很少(实际只有一个),而 threading 模块则很多。
    3. 避免使用 thread 模块的另一个原因是它对于进程如何退出没有控制。当主线程结束时,所有其他线程也都强制结束,不会发出警告或者进行适当的清理。但至少 threading 模块能确保重要的子线程在进程退出前结束。

2、thread 模块

除了派生线程外,thread 模块还提供了基本的同步数据结构,称为锁对象(也称为原语锁、简单锁、互斥锁、互斥和二进制信号量)。下表展示了一些常用的线程函数以及 LockType 锁对象的方法:

函数/方法 说明
thread 模块的函数
start_new_thread(function,args,kwargs=None) 派生一个新的线程,使用给定的 args 和可选的 kwargs 来执行 function
allocate_lock() 分配 LockType 锁对象
exit() 给线程退出指令
LockType 锁对象的方法
acquire(wait=None) 尝试获取锁对象
locked() 如果获取了锁对象则返回 True,否则,返回 False
release() 释放锁

3、threading 模块

除了 Thread 类之外,该模块还包括许多非常好用的同步机制。下表给出了 threading 模块中所有可用对象的列表。

对象 说明
Thread 表示一个执行线程的对象
Lock 锁原语对象
RLock 可重入锁对象,使单一线程可以(再次)获得已持有的锁(递归锁)
Condition 条件变量对象,使得一个线程等待另一个线程满足特定的 “条件”,比如改变状态或某个数据值
Event 条件变量的通用版本,任意数量的线程等待某个事件的发生,在该事件发生后所有线程将被激活
Semaphore 为线程间共享的有限资源提供一个 “计数器”,如果没有可用资源时会阻塞
BoundedSemaphore 与 Semaphore 相似,不过它不允许超过初始值
Timer 与 Thread 相似,不过它要在运行前等待一段时间
Barrier 创建一个 “障碍”,必须达到指定数量的线程后才可以继续
  • 核心提示:守护线程

    避免使用 thread 的另一个原因是该模块不支持守护线程的概念。**当主线程退出时,所有子线程都将终止,不管他们是否正在工作。**如果你不想发生这种行为,就要引入守护线程了。

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

    要将一个线程设置成守护线程,需要在启动线程之前执行如下语句:thread.daemon = True。同样,要检查线程的守护状态,也只需要检查这个值即可。一个新的线程会继承父线程的守护标记。整个 Python 程序将在所有非守护线程退出后才退出

3.1、Thread 类

threading 模块的 Thread 类是主要的执行对象。

属性 说明
Thread 对象数据属性
name 线程名
ident 线程的标识符
daemon 布尔标识,标识这个线程是否是守护线程
Thread 对象方法
_init_(group=None,target(func)=None,name=None,args=(),kwargs={},verbose=None,daemon=None) 实例化一个线程对象,需要有一个可调用的 target,以及其参数 args 或 kwargs。还可以传递 name 或 group 参数,不过后者还未实现。此外,verbose 标志也是可以接受的。而 daemon 的值也将会设定 thread.daemon 属性/标志
start() 开始执行该进程
run() 定义线程功能的方法(通常在子类中被应用开发者重写)
join(timeout=None) 直至启动的线程终止之前一直挂起;除非给出了 timeout,否则会一直阻塞
getName() 返回线程名(更好的方法是设置(或获取)thread.name 属性,或者在实例化过程中传递该属性)
setName() 设定线程名(同上)
isAlivel/is_alive() 布尔标识,表示这个线程是否还存活
isDaemon() 如果是守护线程,则返回 True;否则,返回 False(更好的方法是设置(或获取)thread.daemon 属性,或者在实例化过程中传递该属性)
setDaemon(daemon) 把线程的守护标识设定为布尔值 daemonic(必须在线程 start() 之前调用)(同上)

使用 Thread 类,可以有很多种方法创建线程:

扫描二维码关注公众号,回复: 11812078 查看本文章
  • 创建 Thread 的实例,传给它一个函数;
  • 创建 Thread 的实际,传给它一个可调用的类实例;
  • 派生 Thread 的子类,并创建子类的实例。

3.1.1、派生 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())

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())
starting at: Thu Aug 20 16:09:44 2020
start loop 0 at: Thu Aug 20 16:09:44 2020
start loop 1 at: Thu Aug 20 16:09:44 2020
loop 1 done at: Thu Aug 20 16:09:46 2020
loop 0 done at: Thu Aug 20 16:09:48 2020
all DONE at: Thu Aug 20 16:09:48 2020

3.2、threading 模块的其他函数

函数 说明
active_count() 当前活动的 Thread 对象个数
current_thread 返回当前的 Thread 对象
enumerate() 返回当前活动的 Thread 对象列表
settrace(func) 为所有线程设置一个 trace 函数
setprofile(func) 为所有线程设置一个 profile 函数
stack_size(size=0) 返回新建线程的栈大小;或为后续创建的线程设定栈的大小为 size

4、单线程与多线程的对比

下面的脚本比较了递归求斐波那契、阶乘与累加函数的执行。该脚本按照单线程的方式运行这三个函数。之后使用的多线程的方式执行同样的任务,用来说明多线程的优势。

import threading
from time import ctime,sleep

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):
        print('starting', self.name, 'at:', ctime())
        self.res = self.f unc(*self.args)
        print(self.name, 'finished at:', ctime())

    def getResult(self):
        return self.res

def fib(x):
    sleep(0.005)
    if x < 2:
        return 1
    return (fib(x-2) + fib(x-1))

def fac(x):
    sleep(0.1)
    if x < 2:
        return 1
    return (x * fac(x-1))

def sum(x):
    sleep(0.1)
    if x < 2:
        return 1
    return (x + sum(x-1))

funcs = [fib,fac,sum]
n = 12

nfuncs = range(len(funcs))

print('*** SINGLE THREAD')
for i in nfuncs:
    print('starting',funcs[i].__name__,'at:',ctime())
    print(funcs[i](n))
    print(funcs[i].__name__,'finished at:',ctime())

print('\n*** NULTIPLE THREADS')
threads = []
for i in nfuncs:
    t = MyThread(funcs[i],(n,),funcs[i].__name__)
    threads.append(t)

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

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

print('all Done')
*** SINGLE THREAD
starting fib at: Fri Aug 21 10:17:44 2020
233
fib finished at: Fri Aug 21 10:17:53 2020
starting fac at: Fri Aug 21 10:17:53 2020
479001600
fac finished at: Fri Aug 21 10:17:55 2020
starting sum at: Fri Aug 21 10:17:55 2020
78
sum finished at: Fri Aug 21 10:17:56 2020

*** NULTIPLE THREADS
starting fib at: Fri Aug 21 10:17:56 2020
starting fac at: Fri Aug 21 10:17:56 2020
starting sum at: Fri Aug 21 10:17:56 2020
fac finished at: Fri Aug 21 10:17:57 2020
sum finished at: Fri Aug 21 10:17:57 2020
fib finished at: Fri Aug 21 10:18:05 2020
all Done

以单线程模式运行时,只是简单地依次调用每个函数,并在函数执行结束后立刻显示结果。而以多线程模式运行时,并不会立刻显示结果。我们一直等待所有线程都执行结束,然后调用 getResult() 方法来显示每个函数的结果。


5、同步原语

5.1、信号量机制

原理见《操作系统学习笔记之——进程管理》。

我们将模拟一个简化的糖果机。这个特制的机器只有 5 个可用的槽来保存库存(糖果)。如果所有的槽都满了,糖果就不能再加到这个机器中了;相似的,如果所有的槽都空了,那么买糖果的消费者就无法买到糖果了。

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

lock = Lock()
MAX = 5
candytray = BoundedSemaphore(MAX)

def refill():
    lock.acquire()
    print('Refill candy...',end='')
    try:
        candytray.release()
    except ValueError:
        print('full,skipping')
    else:
        print('OK')
    lock.release()

def buy():
    lock.acquire()
    print('Buying candy...',end='')
    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))

print('starting ay:',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()  #vndr

@register
def _atexit():
    print('all Done at:',ctime())
  • 第 1~4 行

    启动行和导入模块的行与之前的例子非常相似。唯一不同的是信号量。threading 模块包括两种信号量类:Semaphore 和 BoundedSemaphore。信号量实际上就是一个计数器,它们从固定数量的有限资源起始。

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

  • 第 6~8 行

    这个脚本的全局变量包括:一个锁,一个表示库存商品的最大值的常量,一个糖果托盘信号量。

  • 第 10~19 行

    当虚构的糖果所有者向库存中添加糖果时,会执行 refill() 函数。这段代码是一个临界区,这就是为什么获取锁是执行所有行的仅有办法。代码会输出用户的行动,并在某人添加的糖果超过最大库存时给予警告。

  • 第 21~28 行

    buy() 函数它允许消费者获取一个单位的库存。条件语句检测是否所有的资源都已经消费完。计数器的值不能小于 0,因此这个调用一般会在计数器在此增加之前被阻塞。通过传入非阻塞的标志 False,让调用不再阻塞,而在应当阻塞的时候返回一个 False,指明没有更多的资源了。

  • 第 30~38 行

    productor() 和 consumer() 函数只是一个循环,进行对应的 refill() 和 buy() 调用,并在期间暂停。

  • 第 40~48 行

    创建了生产者和消费者的线程对。同时还进行了额外的数学操作,用于随机给出正偏差,使得消费者真正消费的糖果数可能会比生产者放入机器的更多,否则,代码将永远不会进行消费者尝试从空机器购买糖果的情况。

starting ay: Fri Aug 21 15:26:05 2020
THE CANDY MACHINE (FULL WITH 5 BARS!)
Buying candy...OK
Refill candy...OK
Refill candy...full,skipping
Buying candy...OK
Buying candy...OK
Buying candy...OK
Buying candy...OK
Buying candy...OK
all Done at: Fri Aug 21 15:26:11 2020

6、生产者–消费者问题和 queue 模块

在这个场景下,商品或服务的生产者生产商品,然后将其放到类似队列的数据结构中。生产商品的时间是不确定的,同样消费者消费生产者生产的商品的时间也是不确定的。我们使用 queue 模块来提供线程间的通信机制,从而让线程之间可以互相分享数据。具体而言,就是创建一个队列,让生产者(线程)在其中放入新的商品,而消费者(线程)消费商品。

属性 说明
queue 模块
Queue(maxsize=0) 创建一个先入先出的队列。如果给定最大值,则在队列没有空间时阻塞;否则为无限队列
LifoQueue(maxsize=0) 创建一个后入先出的队列。最大值同上
PriorityQueue(maxsize=0) 创建一个优先级队列。最大值同上
queue 异常
Empty 当对空队列调用 get*() 时抛出异常
Full 当对已满队列调用 put*() 时抛出异常
queue 对象方法
qsize() 返回队列的大小
empty() 如果队列为空,则返回 True;否则,返回 False
full() 如果队列为满,则返回 True;否则,返回 False
put(item,block=True,timeout=None) 将 item 放入队列。如果 block 为 True 且 timeout 为 None,则在有可用空间之前阻塞;如果 timeout 为正值,则最多阻塞 timeout 秒;如果 block 为 False,则抛出 Empty 异常
put_nowait(item) 和 put(item,False) 相同
get(block=True,timeout=None) 从队列中取得元素。如果给定了 block(非0),则一直阻塞到有可用的元素为止
get_nowait() 和 get(False) 相同
task_done() 用于表示队列中的某元素已执行完成,该方法会被下面的 join() 使用
join() 在队列中所有的元素执行完成并调用上面的 task_done() 信号之前,保持阻塞
from random import randint
from time import sleep,ctime
from queue import Queue
import threading

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):
        print('starting', self.name, 'at:', ctime())
        self.res = self.func(*self.args)
        print(self.name, 'finished at:', ctime())

    def getResult(self):
        return self.res

def writeQ(queue):
    print('producing object for Q...',end='')
    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))

nloops = randint(2,5)
q =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')
  • 第 1~4 行

    使用了 queue.Queue 对象,另外使用 random.randint() 以使生产和消费的数量有所不同。

  • 第 21~28 行

    writeQ() 和 readQ() 函数分别用于将一个对象(我们这里使用字符串 “xxx”)放入队列中和消费队列中的一个对象。注意,我们这里每次只生产或读取一个对象。

  • 第 30~38 行

    writer() 将作为单个线程运行,其目的只有一个:向对列中放入一个对象,等待片刻,然后重复上述步骤,直至达到每次脚本执行时随机产生的次数为止。reader() 与之类似。

    你会注意到,writer 睡眠的时间通常会比 reader 的时间短。这是为了阻碍 reader 从空队列中获取对象。

starting writer at: Fri Aug 21 16:00:03 2020
producing object for Q...size now: 1
starting reader at: Fri Aug 21 16:00:03 2020
consumed object from Q... size now: 0
producing object for Q...size now: 1
consumed object from Q... size now: 0
producing object for Q...size now: 1
producing object for Q...size now: 2
producing object for Q...size now: 3
consumed object from Q... size now: 2
writer finished at: Fri Aug 21 16:00:12 2020
consumed object from Q... size now: 1
consumed object from Q... size now: 0
reader finished at: Fri Aug 21 16:00:21 2020
All Done

猜你喜欢

转载自blog.csdn.net/qq_36879493/article/details/108149610