一直想写一个多线程博客,汇总一下方老师教给我们的知识。但是因为一直没有用到,或者自己还没有吃透里面的精髓,所以不敢下笔。现在工作中又遇到必须要通过多线程解决的问题,所以再回顾以前方老师的课程,从头整理一下多线程异步这块知识,方便以后遇到问题可以快速写出代码来。
1、多线程异步初步介绍
串行和异步模式如下图,从图上可以很直观看出串行变成和异步编程区别
python 中线程需要用到自带的Threading包,可以使用并发
1.1一般的异步demo
import time
import threading
def syn_method():#串行的普通编程方式
print("Start")
time.sleep(1)
print("Visit website 1")
time.sleep(1)
print("Visit website 2")
time.sleep(1)
print("Visit website 3")
print("End")
def asyn_method():#异步多线程方式
print("Start")
def visit1():#注意,这里是在asyn_method函数内部定义visit1函数,在python中是允许这样写的
time.sleep(1)
print("Visit website 1")
def visit2():
time.sleep(1)
print("Visit website 2")
def visit3():
time.sleep(1)
print("Visit website 3")
th1 = threading.Thread(target=visit1)#首先定义多线程
th2 = threading.Thread(target=visit2)
th3 = threading.Thread(target=visit3)
th1.start()#其次运行线程,相当于快速按下线程开关,线程启动后继续异步向下执行下面代码
th2.start()
th3.start()
th1.join()#最后汇总,等待线程1完成
th2.join()
th3.join()
print("End")
asyn_method()#异步多线程函数
# syn_method()#同步串行函数
上面这个代码包含了线程定义、线程开始、线程阻塞执行三个要点。
1.2傀儡线程
在异步执行的时候,主线程执行完毕后(子线程没有join等待完成),有些子线程可能还没有启动,这时候就需要在主线程执行完毕后把所有没用启动或者正在执行的线程都杀死,避免造成系统垃圾。这时候就要用到傀儡线程,如果设置了傀儡线程,当主线程执行完毕后傀儡线程会自动关闭
傀儡线程的设置很简单,只需要在设置线程的时候增加daemon=True即可
把上面代码做了一下改进,1、把join()阻塞代码注释掉;2、在定义线程的时候增加daemon=True;3把sleep时间增加到10秒,便于更好的实验观察
代码如下:
import time
import threading
def syn_method():#串行的普通编程方式
print("Start")
time.sleep(1)
print("Visit website 1")
time.sleep(1)
print("Visit website 2")
time.sleep(1)
print("Visit website 3")
print("End")
def asyn_method():#异步多线程方式
print("Start")
def visit1():#注意,这里是在asyn_method函数内部定义visit1函数,在python中是允许这样写的
time.sleep(10)
print("Visit website 1")
def visit2():
time.sleep(10)
print("Visit website 2")
def visit3():
time.sleep(10)
print("Visit website 3")
th1 = threading.Thread(target=visit1,daemon=True)#设置daemon=True,把线程定义成傀儡线程,当主线程结束后,傀儡线程自动关闭,而不会在后台运行
th2 = threading.Thread(target=visit2,daemon=True)
th3 = threading.Thread(target=visit3,daemon=True)
th1.start()#其次运行线程,相当于快速按下线程开关,线程启动后继续异步向下执行下面代码
th2.start()
th3.start()
#th1.join()#最后汇总,等待线程1完成
#th2.join()
#th3.join()
print("End")
asyn_method()#异步多线程函数
# syn_method()#同步串行函数
增加了傀儡设置后,运行结果如下
各个子线程的结果没用输出,因为在等待的过程中主线程已经执行完毕了,各个子线程被杀死了。
实验二:把傀儡线程代码daemon=True删掉,或者改为False,结果如下
主线程执行完,但是程序并没有真正结束,等待一会儿子线程结果输出了,程序结束。
2、线程锁
2.1、为什么要锁
为什么要线程锁,只要有线程定义、线程开始、线程阻塞执行三个要点就够了吗?答案是肯定不够的
我们来看一个demo
该demo功能是一个多线程累加和一个多线程累减,
按照上面已经学到的知识可以很轻松写出下面代码
2.2、不加锁代码
import threading
n=0
def add(num):#累加的函数
global n
for _ in range(num):
n+=1
def sub(num):#减的函数
global n
for _ in range(num):
n -= 1
num = 10000000
th_add = threading.Thread(target=add,args=(num,))#定义线程,把num传参进去
th_sub = threading.Thread(target=sub,args=(num,))
th_add.start()#启动线程
th_sub.start()
th_add.join()#阻塞执行
th_sub.join()
print("n=",n)
print("End")
简单分析一下,定义了一个多线程的加函数,又定义了一个多线程减函数,分别启动后阻塞等待程序执行完,不管输入的数字是多少,期待的结果总是为0才对,执行完毕后,结果如下:
n为啥不等于0,多换几个数字,依然不等于0
原因分析:
假设n=100的时候,首先加法线程取出100,然后进行加一操作等于101,但是这时候异步在执行,没有等把101结果返回给n的时候减法函数从内存中取出了n值为100,同样进行了减1操作,结果等于99,这时候99的结果和101结果同时赋值给n,所以这时候n的结果不是99就是101,但是不管结果是99或者101,这个时候n值已经错了(n值原来为100,加了一个数,然后又减了一个数,结果应该还是100才对)。
所以这时候就要用到线程锁来协调数据,在进行加法或者减法操作的时候希望n值是计算完成,写进去了,这样就不会乱了。
先在开头定义一把锁:lock = threading.Lock()。加锁有两种写法,可以用 lock.acquire()加锁,lock.release()解锁方法;或者直接with lock方法。
下面demo分别演示了两种加锁方式
2.3、加锁代码
import threading
lock = threading.Lock()#在开头定义一把锁
n=0
def add(num):#加的函数
global n
for i in range(num):
lock.acquire()#加锁方法一。相当于java 的 lock.lock()
try:
n+=1
finally:
lock.release()#解锁
def sub(num):#减的函数
global n
for _ in range(num):
with lock:#加锁方法二
n -= 1
num = 10000000
th_add = threading.Thread(target=add,args=(num,))#定义线程,把num传参进去
th_sub = threading.Thread(target=sub,args=(num,))
th_add.start()#启动线程
th_sub.start()
th_add.join()#阻塞执行
th_sub.join()
print("n=",n)
print("End")
不管num是多少,结果为0。符合预期结果
3、条件锁
有锁就够了吗,感觉还不够方便。因为如果我有一个缓冲区(缓冲区可以是个有限长度的列表,也类似于一个内存,内存容量是有限的,缓冲区的容量同样是指有限容量的空间),要在缓冲区内想利用多线程写进数据,同时也想从缓冲区获取数据,获取完数据就删除数据,释放缓冲区空间。缓冲区有大小,所以如果已经满了就写不进去,如果缓冲区没用数据,则获取失败。该问题就是生产者和消费者问题,或者也叫缓冲区问题。这时候就要用到条件锁
条件锁的定义方法:
has_data = threading.Condition(lock)#里面要传入一个锁,构成条件锁
has_loc= threading.Condition(lock)
一个lock可以有很多Condition,一个Condition只有一个lock。
Condition里面有个wait方法进行条件阻塞,配合if可以实现带条件的锁定和解锁。
阻塞的wait方法返回条件有两个,一是激活notify_all;二是timeout时间到了;
例如下面一个样例:1个lock,赋给了两个Condition,一个是C1,一个是C2;C1下面又有两个线程与其有关th1、th2;C2下面又有两个线程与其有关th3、th4、th5。如果这时候notify_all C1,则下面的th1和th2都活过来了。如果这时候notify_all C2,则th3、th4、th5都活过来了。
3.1、条件锁代码
下面看看代码
#程序功能:生产者消费者问题
import threading
class Buffer:#定义缓冲区类
def __init__(self,buffer_size:int):
self.buffer_size = buffer_size#缓冲区大小
self.buffer = []#用一个列表模拟缓冲区
lock = threading.RLock()#RLock允许在lock中进行第二次调用acquire()进行锁定,而如果是普通lock,不允许在lock中套入lock的acquire(),这样造成锁死(简单的说:一个锁把带有钥匙的主人给锁住了)。所以一般的RLock比较常用,这种lock相当于java ReentrantLock
self.has_lock = threading.Condition(lock)#条件锁定义,表示有空位
self.has_data = threading.Condition(lock)#条件锁定义,表示有数据
def put(self,data,blocking = True):#向buffer传入数据,生产者。blocking是判定有没有缓存空间写入数据
#return True if the data is put into this buffer successfully,False otherwise
with self.has_lock:
if blocking:
while len(self.buffer)>=self.buffer_size:#条件锁的条件
self.has_lock.wait()#阻塞has_lock
else:
if len(self.buffer) >= self.buffer_size:
return False
self.buffer.append(data)
self.has_data.notify_all()#激活has_data条件锁
return True
def get(self,blocking = True):#取数据(消费者)
with self.has_data:
if blocking:
while len(self.buffer)==0:#取数据等待的条件是缓冲区没用数据
self.has_data.wait()#阻塞has_data
else:
if len(self.buffer)==0:
return False
result = self.buffer[0]
del self.buffer[0]
self.has_lock.notify_all()#删除了取出的数据,说明有空位了,这时候激活has_lock
return result
if __name__ == '__main__':
num = 20
buffer= Buffer(5)
def produce(n:int):#生产函数
for i in range(num):
data = "data_%d_%d"%(n,i)
buffer.put(data)
print(data,"is produced.")
def resume():#消费函数
for _ in range(num):
data = buffer.get()
print(data," is resume.")
th0 = threading.Thread(target=produce,args=(0,))#定义一个多线程,调用生产的函数
th1 = threading.Thread(target=produce,args=(1,))
th2 = threading.Thread(target=resume)#定义一个多线程,调用消费的函数
th3 = threading.Thread(target=resume)
th0.start()
th1.start()
th2.start()
th3.start()
th0.join()
th1.join()
th2.join()
th3.join()
print("The test is End")
就这样吧,后面再在开新文章,增加几个多线程案例。