在网络与并发编程01中简单介绍了各种基础的概念,现在继续介绍一些概念,在网络与并发编程01中最后的代码实现中,可以发现主线程并未在子线程结束后才结束,如下图所示:主线程结束后,子线程还在继续运行
为了解决这一问题,就引入join()线程联合,即需要等待子线程结束后,主线程才结束,就是在网络与并发编程01的代码中添加如下两行,如下图所示:
结果也发生变化,如图:
守护线程
在行为上还有一种守护线程,主要的特征是它的生命周期,主线程死亡,它也随之死亡。守护线程作用是为其他线程提供便利服务,守护线程最典型的应用就是 GC (垃圾收集器)。
实现方式如下:
正常运行结果还要输出线程t1:1、 线程t1:2,但是这里并没有输出,就说明线程在主线程结束后,线程t1已死亡。
线程同步和互斥锁
假想这样一种场景,多个线程访问同一个对象资源,并且修改了对象,比如两个人同时取一张银行卡里面的钱,就可能造成两个人取的钱的总额大于该银行卡里面的总额,理解得不是太清楚,没关系,看下面具体代码就理解了
# 未加互斥锁
from threading import Thread
from time import sleep
class Account():
def __init__(self, money, name):
self.money = money
self.name = name
# 模拟取款操作
class Drawing(Thread):
def __init__(self, drawingNum, account):
super().__init__()
self.drawingNum = drawingNum
self.account = account
self.expenseTotal = 0
def run(self):
if self.account.money - self.drawingNum<0:
return
sleep(1) # 判断完可以取钱,由于这里sleep,另一个线程也开始运行,故意造成阻塞,就是为了测试发生冲突,在第二个线程开始运行时,第一个线程尚未取钱完成,所以这里第二个线程可以顺利通过取钱判断
self.account.money -= self.drawingNum
self.expenseTotal += self.drawingNum
print(f"账户:{self.account.name}, 余额:{self.account.money}")
print(f"账户:{self.account.name}, 共取:{self.expenseTotal}")
if __name__ == '__main__':
a1 = Account(100, 'cjj')
draw1 = Drawing(80, a1) # 定义一个取钱的线程
draw2 = Drawing(80, a1) # 定义一个取钱的线程
draw1.start()
draw2.start()
可以看到,本来账户只有100块,但是2个人都能取钱,造成银行账户还欠60块,这显然是不现实的,通过这我们就可以发现,当有多个线程调用对象时,特别是想修改对象时,最好还是加一个互斥锁,让他们不能同时“操作”该对象,具体方法如下:
from threading import Thread, Lock
from time import sleep
class Account():
def __init__(self, money, name):
self.money = money
self.name = name
class Drawing(Thread):
def __init__(self, drawingNum, account):
super().__init__()
self.drawingNum = drawingNum
self.account = account
self.expenseTotal = 0
def run(self):
lock1.acquire() # 拿到锁,才能调用线程
if self.account.money - self.drawingNum < 0:
print('账户余额不足')
return
sleep(1)
self.account.money -= self.drawingNum
self.expenseTotal += self.drawingNum
lock1.release() # 释放锁,让下面运行的线程可以拿到
print(f"账户{self.account.name}, 余额{self.account.money}")
print(f"账户{self.account.name}, 共取{self.expenseTotal}")
if __name__ == '__main__':
a1 = Account(100, 'cjj')
lock1 = Lock() # 获取锁对象
draw1 = Drawing(80, a1)
draw2 = Drawing(80, a1)
draw1.start()
draw2.start()
第二个线程想取钱时,直接因为余额不足,取不了,这就是互斥锁的作用。
使用互斥锁的作用如下:
1、必须使用同一个锁对象
2、互斥锁的作用就是保证同一时刻只能有一个线程去操作共享数据,保证共享数据不会出现错误 问题
3、使用互斥锁的好处确保某段关键代码只能由一个线程从头到尾完整地去执行
4、使用互斥锁会影响代码的执行效率
5、同时持有多把锁,容易出现死锁的情况
使用互斥锁就可能出现“死锁”情况,那么什么是死锁情况呢?
通过上图我们简单回答一下什么是“死锁”,线程A要同时拿到锁1和锁2才能调用资源,线程B也要同时拿到锁1和锁2才能调用资源 ,现在的问题就是线程A先拿到锁1,线程B先拿到了锁2,线程B没有拿到锁1无法进行程序,也就无法释放锁2;同理,线程A也无法释放锁1,这样双方就尬住了,无法完成整个程序,就造成了“死锁”问题。
from threading import Thread, Lock
from time import sleep
def func1():
lock1.acquire()
print('fun1拿到菜刀')
sleep(1)
lock2.acquire()
print('fun1拿到锅')
sleep(1)
lock2.release()
print('fun1放下锅')
lock1.release()
print('fun1放下菜刀')
def func2():
lock2.acquire()
print('fun2拿到锅')
sleep(1)
lock1.acquire()
print('fun1拿到菜刀')
sleep(1)
lock1.release()
print('fun1放下菜刀')
lock2.release()
print('fun1放下锅')
if __name__ == '__main__':
lock1 = Lock()
lock2 = Lock()
t1 = Thread(target=func1,) # 线程t1拿到lock1
t2 = Thread(target=func2,) # 线程t2拿到lock2
t1.start()
t2.start()
可以看见线程t1、t2都卡住不继续往下运行了,这就是死锁问题。
信号量
信号量控制同时访问资源的数量。信号量和锁相似,锁同一时间只允许一个对象(进程)通过,信号量同一时间允许多个对象(进程)通过。
# 一个房子,一次只允许两个人进
from threading import Semaphore
from threading import Thread
from time import sleep
def home(name, se):
se.acquire()
print(f'{name}进入房间')
sleep(1.66)
print(f'****{name}走出房间')
se.release()
if __name__ == '__main__':
se = Semaphore(2) # 信号量对象
for i in range(7):
t = Thread(target=home, args=(f'tom{i}', se))
t.start()
可以看到永远保证2个线程调用同一资源,一个走出“房间”,释放资源,另一个线程马上进入“房间”,调用资源。
事件
事件Event主要用于唤醒正在阻塞等待状态的线程。
Event 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,event 对象中的信号标志被设置假。如果有线程等待一个 event 对象,而这个 event 对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个 event 对象的信号标志设置为真,它将唤醒所有等待这个 event 对象的线程。如果一个线程等待一个已经被
设置为真的 event 对象,那么它将忽略这个事件,继续执行。
Event() 可以创建一个事件管理标志,该标志(event)默认为False,
event对象主要有四种方法可以调用:
from threading import Thread, Event
import time
def chihuoguo(name):
# 等待事件,进入等待阻塞状态
print(f"{name}已经启动")
print(f"小伙伴{name}已经进入就餐状态")
time.sleep(1)
event.wait()
# 收到事件后进入运行状态
print(f"{name}收到通知了")
print(f"小伙伴{name}开始吃咯!")
if __name__ == '__main__':
event = Event()
t1 = Thread(target=chihuoguo, args=('tom', ))
t2 = Thread(target=chihuoguo, args=('cherry', ))
t1.start()
t2.start()
time.sleep(10)
# 发送事件通知
print('------->>>主线程通知小伙伴开始吃咯!')
event.set()
可以自己尝试一下,把最后一行的event.set()注释掉,会发现线程永远在等待被唤醒,不会向下执行程序。
生产者和消费者模式
生产者:指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)
消费者:指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)
缓冲区:消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。
from queue import Queue
from threading import Thread
from time import sleep
def producer():
num = 1
while True:
if queue.qsize() < 5:
print(f'生产{num}号cpu')
queue.put(f"cpu:{num}号")
num += 1
else:
print('CPU订单满了,等待别人来买!')
sleep(1)
def consumer():
while True:
print(f"获取cpu:{queue.get()}")
sleep(1)
if __name__ == '__main__':
queue = Queue()
t1 = Thread(target=producer)
t2 = Thread(target=consumer)
t1.start()
t2.start()
只截取了部分运行结果,它这里是无限循环下去,所以会一直执行下去 。
这就是线程的大部分相关知识,下面补充一下全局锁GIL问题。
Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。但是GIL并不是Python的特性,而是Python解释器CPython的问题。