Python高级培训:多线程
什么是多线程
多线程的定义:线程是有程序执行的最小单位,而进程是操作系统分配资源的最小单位;一个进程有一个或多个线程组成,线程是一个进程中代码的不同执行路线;进程之间相互独立,但统一进程下的各个线程之间共享程序的存储空间(包括代码段,数据集,堆等)及一些进程的资源(如打开文件和信号等),某进程内的线程在其他进程中不可见;调度和转换:线程上下文切换比进程上下文切换要快得多
多线程的原理:每个程序都有一个主入口一样多线程中也一定有一个主线程,当程序开始运行时便开始执行主线程中的代码,其中某些代码可能会导致一些副线程的开启,但是副线程的开启并不会影响主线程的进行,而是被发配到后台执行
多线程的优势:多线程可以尽可能地利用处理器的资源;将耗时长、运算量大的任务放在后台执行将精力集中于更重要的事情上这样效率更高
threading 模块
Python3通过两个标准库 _thread 和 threading 提供对线程的支持,但 _thread 模块老旧、功能较少而 threading 模块是Python3新建立的模块功能丰富,所以接下来所有功能的实现均采用 threading 模块
相比于 _thread , threading 模块除了包含 _thread 模块中的所有方法外,还提供的其他方法:
-
threading.currentThread()
:返回当前的线程变量。 -
threading.enumerate()
返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 -
threading.activeCount()
返回正在运行的线程数量。
除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
run()
用以表示线程活动的方法。start()
启动线程活动。join()
等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。isAlive()
返回线程是否活动的。getName()
返回线程名。setName()
设置线程名。
创建线程
直接创建并开始运行线程
import threading
import time
# 定义线程需要做的job
def job(n):
print('这是一个子线程的开始')
time.sleep(n)
print('这是一个子线程的结束')
# 定义main函数
def main():
print('主线程任务开始')
t = threading.Thread(target=job, args=(3, )) # 一个线程对象被直接创建
t.start() # t线程开始执行
print('主线程任务结束')
# 定义程序的入口为main函数
if __name__ == '__main__':
main()
'''
if __name__ == '__main__':的作用在于若该文件被直接运行(作为主程序运行)就可以执行 if 内的语句,如果作为模块被别的文件导入(并非主程序)则不会执行 if 内的语句
''''''
输出结果会出现两种情况
情况1: | 情况2:
主线程任务开始 | 主线程任务开始
这是一个子线程的开始 | 这是一个子线程的开始主线程任务结束
主线程任务结束 | # 等待3秒
# 等待3秒 | 这是一个子线程的结束
这是一个子线程的结束 |
| Process finished with exit code 0
Process finished with exit code |
'''
为什么会出现两种情况呢?经过我的思考后我得出了这样的结论:
根本原因在于线程之间是相互独立的,为什么这么这么说呢,因为在上面的代码执行到t.start()
时程序便出现了分支,如图所示,当t.start()
执行完毕后子线程开始执行同时主线程也没有停止两个线程相互独立均执行他们的下一条语句,其中主线程的下一条语句是print('主线程任务结束')
;子线程的下一条语句是print('这是一个子线程任务的开始')
两条语句理应同时执行,但由于线程之间相互独立(处理速度也相互独立)这就导致了会出现以上两种情况,两个print会有先后顺序(其实应该是三种情况,但目前我的知识水平有限只能做出这样的解释,不再深究)
我的观点可以被证明,既然无法得知两个print的先后那就在主线程的结束print前添加一个time.sleep(1)
这样就能够区分顺序了
import threading
import time
# 定义线程需要做的job
def job(n):
print('这是一个子线程的开始')
time.sleep(n)
print('这是一个子线程的结束')
def main():
print('主线程任务开始')
t = threading.Thread(target=job, args=(3, )) # 一个线程对象被直接创建
t.start() # t线程开始执行
time.sleep(1)
print('主线程任务结束')
if __name__ == '__main__':
main()
'''
输出结果理应也确实为:
主线程任务开始
这是一个子线程的开始 # 等待1s主线程结束
主线程任务结束 # 再等待2s子线程结束
这是一个子线程的结束
Process finished with exit code 0
'''
上面的例子充分体现了多线程的特点
在类中创建并开始运行线程
与直接创建线程类似,只是自己创建一个类继承自Threading.Thread
需要注意的是类中需要定义一个run()
方法(方法名不能改,必须叫run),它的作用相当于直接创建线程中的job
import threading
import time
class Mythread(threading.Thread):
def __init__(self, thread):
threading.Thread.__init__(self)
self.thread = thread
def run(self):
print(self.thread + ' -- ' + '|子线程开始|')
while 1:
print(self.thread + ' -- |这是子线程正在做的job|')
time.sleep(1)
# print(self.thread + '结束')
def main():
print('主进程 -- |主线程开始|')
thread = Mythread('thread1')
thread.start()
time.sleep(0.01)
print('主进程 -- |主线程结束|')
if __name__ == '__main__':
main()
'''
输出的结果为:
主进程 -- |主线程开始|
thread1 -- |子线程开始|
thread1 -- |这是子线程正在做的job|
主进程 -- |主线程结束|
thread1 -- |这是子线程正在做的job|
thread1 -- |这是子线程正在做的job|
# 每秒循环打印
'''
守护线程
如果说主线程是秦始皇的话那么被设为守护线程的线程就是兵马俑,因为当秦始皇(主线程结束)gg了那么他的士兵就要给他陪葬变成兵马俑(不管守护线程结没结束都会随着主线程的结束而结束)
下面这个例子,这里使用setDaemon(True)
把所有的子线程都变成了主线程的守护线程, 因此当主线程结束后,子线程也会随之结束,所以当主线程结束后,整个程序就退出了。
import threading
import time
def run():
print('task')
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
if __name__ == '__main__':
t = threading.Thread(target=run)
# t.setDaemon(True)
t.start()
print('end')
若不给t线程设置守护线程模式输出为:
task end # 等待1s后打印2s 2s # 等待1s后打印1s 1s
Process finished with exit code 0
import threading
import time
def run():
print('task')
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
if __name__ == '__main__':
t = threading.Thread(target=run)
t.setDaemon(True)
t.start()
print('end')
若给t线程设置守护线程模式输出为:
task end
Process finished with exit code 0
如果不想出现上面的情况可以在主线程结束前加入join()
将子线程join到主线程中等待子线程都完成后再结束主线程
多线程共享全局变量
线程是进程的执行单元,进程是系统分配资源的最小执行单位,所以在同一个进程中的多线程是共享资源的 在同一个进程中两个线程可以更改或者读取同一个global变量,例如
import threading
g = 100
def work1():
global g
g+=3
print('in Thread1 g is : %d' % g)
def work2():
global g
print('in Thread2 g is : %d' % g)
if __name__ == '__main__':
t1 = threading.Thread(target=work1)
t1.start()
time.sleep(1)
t2=threading.Thread(target=work2)
t2.start()
输出为: in Thread1 g is : 103 in Thread2 g is : 103
线程锁
线程锁的定义:由于不同线程之间可以共享全局变量,这就会产生一种特殊的情况,多个线程如果同时操作一个变量会发生什么?如果没有很好地保护该对象,会造成程序结果的不可预期,这也被称为“线程不安全”所以出现了线程锁。
互斥锁
同一时刻只允许一个线程执行某些操作。
一个有互斥锁的线程例子:
def work():
global n
lock.acquire() # 当一个线程在操作某一变量时为防止其他线程操作该变量,利用lock.acquire()将该变量锁在本线程中
temp = n
time.sleep(0.1)
n = temp-1
lock.release() # 当操作结束后再利用lock.release()将该变量的锁释放,允许其他线程对该变量进行操作
递归锁
RLcok类的用法和Lock类一模一样,但它支持嵌套。 RLock类代表可重入锁(Reentrant Lock)。 对于可重入锁,在同一个线程中可以对它进行多次锁定, 也可以多次释放。如果使用 RLock,那么 acquire() 和 release() 方法必须成对出现。 如果调用了 n 次 acquire() 加锁,则必须调用 n 次 release() 才能释放锁。 由此可见,RLock 锁具有可重入性。也就是说,同一个线程可以对已被加锁的 RLock 锁再次加锁, RLock 对象会维持一个计数器来追踪 acquire() 方法的嵌套调用, 线程在每次调用 acquire() 加锁后,都必须显式调用 release() 方法来释放锁。 所以,一段被锁保护的方法可以调用另一个被相同锁保护的方法。
信号量
互斥锁同时只允许一个线程更改数据,而Semaphore(信号量)是同时允许一定数量的线程更改数据。类似于动态NAT,比如一家公司有10名员工但只有3个公网IP,所以同时上网的只能有3个人只有一个人不上了其他需要上网的才能得到公网IP上网。
利用threading类Semaphore方法设置信号量(给这个公司配值5个公网IP):threading.Semaphore(5)
在线程中设置对应信号量的锁(同时有5台主机能上网):semaphore.acquire()
加锁semaphore.release()
解锁
举个例子:
import threading
import time
def run(n,semaphore):
semaphore.acquire() # 加锁
print('run the thread:%s\n' % n)
time.sleep(3)
print('over the thread:%s\n' % n)
semaphore.release() # 释放
if __name__ == '__main__':
num = 0
semaphore = threading.Semaphore(5) # 最多允许5个线程同时运行
for i in range(22):
t = threading.Thread(target=run, args=('t-%s' % i, semaphore))
t.start()
while threading.active_count() != 1:
pass
else:
print('----------all threads done-----------')
运行过程应该是:首先thread 0~5会全部开始执行,因为信号量被设为5,三秒过后thread 0~4全部执行完毕,thread 5~9会涌入。直到22个thread全部完成
条件变量
在讲条件变量之前,有必要说说线程中挂起和阻塞,线程的这两个状态乍一看都是线程暂停不再继续往前运行,但是引起的原因不太一样,下面来说说两者的区别。
- 挂起是一种主动的行为,在程序中我们主动挂起某个线程然后可以主动放下让线程继续运行。(为了统一调度)
- 阻塞更多时候是被动发生的,当有线程操作冲突了那么必然是有一方要被阻塞的。(为了防止线程操作冲突)
- 此外一个线程可以在阻塞的时候被挂起,然后被唤醒后依然是阻塞状态。(挂起优先级高)
对于挂起的控制python采用了条件变量Condition,其中的一些重要的方法:
threading.Condition.acquire()
上锁(同lock)
threading.Condition.wait()
释放锁后挂起
threading.Condition.notify()
唤醒其他线程的挂起状态
threading.Condition.release()
释放锁(同lock)
举一个例子:现在有个大学生要用钱,但是银行卡里没有钱,他骗钱的流程有以下几步
- 告知他的父母“我卡里没钱了给我转点钱”
- 父母得知了孩子的困难给孩子转了生活费,父母告知他们的孩子“给你转过去了”
- 孩子收到了父母的转账,告知父母自已已经收到
- 父母收到孩子已经收到钱的消息,再向孩子发出已经得知你收到钱的信息(这件事已经结束,可以结束线程了)
- 孩子收到父母已经收到我收到钱的信息,再向父母发出已经得知你们得知我收到钱的信息(这件事已经结束,可以结束线程了)
代码实现:
import threading
import time
class Parents(threading.Thread):
def __init__(self, cond, name):
threading.Thread.__init__(self)
self.cond = cond
self.name = name
def run(self):
time.sleep(1) # 1.确保parents晚于kid开始执行
self.cond.acquire() # 4. kid的锁释放了所以这里获得了锁
print('父母 行我们知道了,这就给你转。')
print('父母 转帐中。。。')
print('xxx收到来自于xxx的转账500元,余额500元')
print('父母 给你转过去了')
self.cond.notify() # 5.kid线程此时被唤醒并试图获取锁,但是锁还在parents身上,所以kid被阻塞,parents继续往下
self.cond.wait() # 6. parents锁被释放并且挂起,kid就获取锁开始继续往下运行了
print('父母 我们知道你收到了')
self.cond.notify() # 9.找到了之后再次通知kid,kid意图获取锁但不行所以被阻塞,parents往下
self.cond.release() # 10.释放锁
print('父母 线程结束')
class Kid(threading.Thread):
def __init__(self, cond, name):
threading.Thread.__init__(self)
self.cond = cond
self.name = name
def run(self):
self.cond.acquire() # 2.kid获取锁
print('xxx 爸妈我这段时间太难了,没钱吃饭了')
self.cond.wait() # 3.kid被挂起然后释放锁
print('xxx 我收到了')
self.cond.notify() # 7.收到后通知parents收到钱了,parents意图获取锁,但是锁在kid身上所以parents被阻塞
self.cond.wait() # 8.kid被挂起,释放锁,parents获取锁,parents继续往下运行
print('xxx 我知道你们知道我收到了')
self.cond.release() # 11. 在此句之前一点,parents释放了锁(#10),kid得到锁,随即这句kid释放锁
print('xxx 线程结束')
cond = threading.Condition()
parents = Parents(cond, 'Parents')
kid = Kid(cond, 'Kid')
parents.start()
kid.start()
'''
结果:
xxx 爸妈我这段时间太难了,没钱吃饭了
父母 行我们知道了,这就给你转。
父母 转帐中。。。
xxx收到来自于xxx的转账500元,余额500元
父母 给你转过去了
xxx 我收到了
父母 我们知道你收到了
父母 线程结束
xxx 我知道你们知道我收到了
xxx 线程结束
'''
想想看,这个过程有点类似于半双工(只有一把条件锁)的TCP/IP协议的三次握手,一方的动作完成后会进入等待的状态(挂起,释放锁)直到收到对方做出的反馈(解除挂起,获得锁)才继续向下进行(可能再次挂起,释放锁)。上面的例子已经很好的体现了线程中条件变量的特点。
事件
python线程的事件用于主线程控制其他线程的执行,事件是一个简单的线程同步对象,Event 其实就是一个简化版的 Condition 以下是 事件Event 中的一些重要方法
threading.Event.set()
将flag设为True
threading.Event.clear()
将flag设为False
threading.Event.is_set()
获取flag状态,返回True或False
threading.Event.wait()
方法只有在flag为真的时候才会向下执行并完成返回,当flag假时,wait方法会一直监听,如果没有检测到flag变为真,就一直处于阻塞状态。
举个例子:
import threading,time
def car():
while 1:
if event.is_set():
print('小车行驶')
else:
print('小车停止')
event.wait()
def set_event():
while 1:
event.set()
time.sleep(1)
event.clear()
time.sleep(1)
if __name__ == '__main__':
event = threading.Event()
a = threading.Thread(target=car)
b = threading.Thread(target=set_event)
a.start()
b.start()
'''
输出结果为:
小车行驶
小车行驶
.......
小车行驶
小车行驶
小车停止 # 等待1s,循环上面的输出
'''
上面的例子较好的体现了事件的特点
定时器
定时器timer:简单来说就是经过多长的事件启动一个线程
举个例子:
import threading
def run():
print('一个线程被启动了')
t = threading.Timer(5, function=run)
t.start()
程序开始运行,等待5s后打印’一个线程被启动了’
上面就是所具有的特点,有哪些用途呢?举个例子:将定时器用作闹钟每经过5s就提醒一次’Times up!’
import threading
import time
def run():
t1 = threading.Timer(5, function=run)
print('Times up!\n')
time.sleep(1)
t1.start()
print('5s钟后将会叫醒你')
print('5s钟后将会叫醒你')
t = threading.Timer(5, function=run)
t.start()
队列
队列操作在python中有queue中的Queue类来实现。
以下是queue.Queue中的一些方法:
queue.Queue()
: 先入先出
queue.LifoQueue()
: 后入先出
queue.PriorityQueue()
: 设置优先级
Queue.put()
: 向队列中放数据,block=True(是否排队),Timeout=15(排队超时时间),无排队或者超时都会报错
Queue.get()
: 向队列中拿数据,block=True(是否等待), Timeout=15 (空超时时间),无数据或者超时都会报错
Queue.empty()
: 判断队列是否空
Queue.qsize()
: 获取数据的数量
举个例子:
import queue
import threading
import time
def put():
n=1
while True:
q.put('放入1个线程')
print(f'放入线程{n}')
time.sleep(1)
n+=1
def get():
while True:
q.get()
print(f'拿出一个线程开始运行')
time.sleep(1)
print('该线程运行完毕')
if __name__ == '__main__':
q = queue.Queue()
p = threading.Thread(target=put)
p.start()
g = threading.Thread(target=get)
g.start()
'''
放入线程1 # put
拿出一个线程开始运行 # get
该线程运行完毕
放入线程2 # put
拿出一个线程开始运行 # get
该线程运行完毕
放入线程3 # put
拿出一个线程开始运行 # get
该线程运行完毕
放入线程4 # put
拿出一个线程开始运行 # get
'''
线程池
线程池的特点:线程池在系统启动时即创建大量空闲的线程,程序只要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行它。当该函数执行结束后,该线程并不会死亡,而是再次返回到线程池中变成空闲状态,等待执行下一个函数。
线程池可以提高性能,防止启动大量线程而导致系统变慢,可以更简单的常见线程主要用于在突发的需要大量线程,而线程存在时间段的场景
Exectuor 提供了如下常用方法:
submit(fn=, arg=,)
将 fn 函数提交给线程池。
map(fn, )
该函数将会启动多个线程,以异步方式立即对iterables 执行 map 处理。
shutdown(wait=)
关闭线程池。
Future 提供了如下方法:
cancel()
取消该 Future 代表的线程任务。如果该任务正在执行,不可取消返回False;否则,程序会取消该任务返回 True。
cancelled()
返回 Future 代表的线程任务是否被成功取消。
running()
如果该 Future 代表的线程任务正在执行、不可被取消,该方法返回 True。
done()
如果该 Future 代表的线程任务被成功取消或执行完成,则该方法返回 True。
result()
获取该 Future 代表的线程任务最后返回的结果。如果 Future代表的线程任务还未完成,该方法将会阻塞当前线程
exception()
获取该 Future代表的线程任务所引发的异常。如果该任务成功完成,没有异常,则该方法返回 None。
add_done_callback(fn)
为该 Future 代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该fn 函数。
如果使用线程池/进程池来管理并发编程,那么只要将相应的 task 提交给线程池/进程池,剩下的事情就由线程池/进程池来搞定。