系列文章目录
文章目录
一、死锁与递归锁
-
死锁的概念:
死锁是一种现象,会使整个程序永远阻塞下去。
举个例子:当你和你的基友(多个进程)去吃饭,结果老板只给了一双筷子(有限的临界资源),你俩手疾眼快,各自抢到了一根(加锁)。但是,一根筷子没法用来吃饭,而你和你的基友都在等待对方将手里的筷子让给自己,双方进入僵持状态(死锁),这就是一种现实生活中的死锁现象。
-
递归锁的概念:
递归锁(Recursive Lock)也称为可重入互斥锁(reentrant mutex),是互斥锁的一种。
它的特殊之处在于:同一线程对其多次加递归锁不会产生死锁。递归锁会使用引用计数机制,每次加锁时计数加一,释放锁时减一;只要计数不为0,其他线程都无法抢到该锁。
-
递归锁的使用:
multiprocessing
和threading
模块都支持递归锁RLock
from threading import Thread, Lock, RLock import time mutexA = mutexB = RLock() class MyThead(Thread): def run(self): self.func1() self.func2() def func1(self): mutexA.acquire() print('%s 抢到A锁'% self.name) # 获取当前线程名 mutexB.acquire() print('%s 抢到B锁'% self.name) mutexB.release() mutexA.release() def func2(self): mutexB.acquire() print('%s 抢到B锁'% self.name) time.sleep(2) mutexA.acquire() print('%s 抢到A锁'% self.name) # 获取当前线程名 mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(10): t = MyThead() t.start()
二、信号量(semaphore)
信号量是一个内置的计数器,可以用来控制并行的线程数/进程数,超出的线程/进程阻塞,直到有线程/进程运行完成,才运行下一个线程/进程。有点像厕所里的坑位。
import time
from threading import Thread, Semaphore
# 信号量设置为5,则同时最多有5个线程运行
s1 = Semaphore(5)
def foo():
s1.acquire()
time.sleep(2) #程序休息2秒
print("ok",time.ctime())
s1.release()
for i in range(20):
t1 = Thread(target=foo,args=()) #实例化一个线程
t1.start() #启动线程
t1.join()
foo()
三、Event事件
Event()
类的实例对象用来在某个事件发生后,触发另一个事件。这个类在multiprocessing
和threading
模块中都有实现。
from threading import Thread, Event
import time
event = Event()
def li_si():
"""等待张三离开的李四"""
print('李四和张三说话')
time.sleep(5)
print('张三走了') # 事件发生
# 张三走了,给老王发消息
event.set()
def lao_wang():
"""躲在床下的老王"""
print('老王躲在床底下')
# 等待李四给他发消息(阻塞,直到event.set被执行)
event.wait()
print('老王从床下爬出,和李四偷喝张三家的酒') # 触发另一事件
if __name__=='__main__':
t1 = Thread(target=li_si)
t2 = Thread(target=lao_wang)
t1.start()
t2.start()
四、进程池与线程池
理论上,进程/线程都是可以无限新建的,但因为计算机硬件支持的进程数/线程数是有限的,所以我们需要对进程/线程的数量进行限制。而最常用的解决方案就是进程池/线程池。
-
池的概念:
池(pool)是一种技术。它虽然会限制程序的运行效率,但能够保护计算机系统支持运行。上面对并发的限制是池技术应用的一个方面,其他的池有:连接池、半连接池、内存池、对象池等。
1. 进程池的使用
进程池内的进程是固定的,一旦创建就不会销毁,会一直重复使用。即,一个已有进程不会因为自己的任务结束而销毁,解释器会给它重新分派任务去执行。
from concurrent.futures import ProcessPoolExecutor
# 创建进程池,括号内为进程数,默认为cpu的内核数
pool = ProcessPoolExecutor()
def foo():
"""任务"""
return 100
# l用来保存submit的返回值,而submit用来向进程池提交任务
l = []
# windows下需要在main中创建进程
if __name__ == '__main__':
# 向进程池提交20个任务
for i in range(20):
res = pool.submit(foo) # 异步提交,主进程不会等待子线程完成
# 因为获取任务返回值的操作,会使for循环阻塞,变成同步串行执行
# 所以先将submit返回的结果对象保存,之后再获取任务的返回值
l.append(res)
# 关闭进程池,会等待进程池中所有进程将任务执行完
pool.shutdown()
# 获取任务的返回值
for res in l:
# 对submint返回的结果对象,调用result方法获取任务返回值
print(res.result())
2. 线程池的使用
线程池的特性和用法几乎与进程池一样,线程池内的线程也是固定的,也会被重复使用。
上面使用列表获取任务返回值的方法不太合理,实际上,进程池和线程池都有个回调方法,可以更加方便的,异步获取任务返回值:
from concurrent.futures import ThreadPoolExecutor
# 创建线程池,括号内为线程数,默认为cpu内核数的5倍
pool = ThreadPoolExecutor()
def foo():
"""任务"""
return 100
def call_back(n): # 接收的参数为submit返回的对象
"""回调方法"""
# 获取任务返回值
n.result()
for i in range(20):
# 添加回调方法
res = pool.submit(foo).add_done_callback(call_back)
# 关闭线程池
pool.shutdown()
当一个任务完成时,submit方法就会自动调用call_back方法,并将返回值传递给它。
五、协程(Coroutine)和gevent模块
1. 协程的概念和实现
-
协程的概念:
协程不同于进程和线程,它是应用程序员通过代码实现的,而后面二者则是操作系统提供的系统调用。
协程运行在线程之上,是轻量级的线程。它可以随意切换任务,比如:当某个函数阻塞时,可以转而去执行另一个函数,等执行完或阻塞后,又转回原来的函数继续执行。这样,可以提高单线程下cpu的利用率。
-
实现协程:
实现协程最重要的有两点:切换和保存现场,因此可以通过yield关键字来实现。
下面的例子中,
foo
和bar
函数会来回切换执行:import time def foo(): while True: 1 + 1 yield # 会保存现场,去执行bar函数 def bar(): # 初始化生成器 g = foo() for i in range(100000): i + 1 next(g) # 会切换到foo函数 start_time = time.time() bar() print(time.time() - start_time)
2. gevent模块的基本使用:
通常我们只会在遇到阻塞时,主动切换正在执行的任务,而gevent模块,可以帮助我们,监测阻塞。
该模块是第三方模块,需要手动安装:pip install gevent
from gevent import spawn, monkey
import time
# 猴子补丁,打了这个补丁,gevent模块才会生效
monkey.patch_all()
def foo():
"""任务1"""
print('任务1开始')
time.sleep(2)
print('任务1结束')
def bar():
"""任务2"""
print('任务2开始')
time.sleep(3)
print('任务2结束')
start_time = time.time()
# 监测阻塞,传入要监测的函数
# 这两个函数,一个阻塞,协程就回去执行另一个
f = spawn(foo)
b = spawn(bar)
# 等待被检测的任务执行完毕,再去执行后续代码
f.join()
b.join()
# 打印完成两个任务花费的时长
print(time.time()-start_time)
# 打印:3.012303590774536
使用gevent,可以获得极高的并发性能,但gevent只能在Unix/Linux下运行,在Windows下不保证正常安装和运行。