今日学习目标
- 学习进程池与线程池、死锁与事件、协程的知识点以及代码实现
文章目录
学习内容
- GIL与普通互斥锁区别
- 多线程与多进程
- 死锁现象
- 信号量与event事件(了解)
- 进程池与线程池
- 协程
一、GIL与普通互斥锁区别
1.验证GIL的存在
from threading import Thread, Lock
import time
money = 100
def task():
global money
money -= 1
for i in range(100): # 创建一百个线程
t = Thread(target=task)
t.start()
print(money)
'''
# 执行结果
0
'''
2.验证不同数据加不同锁
from threading import Thread, Lock
import time
money = 100
mutex = Lock()
def task():
global money
mutex.acquire()
tmp = money
time.sleep(0.1)
money = tmp - 1
mutex.release()
t_list = []
for i in range(100): # 创建一百个线程
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
# 为了确保结构正确 应该等待所有的线程运行完毕再打印money
print(money)
'''
# 执行结果
0
'''
总结:
- GIL是一个纯理论知识 在实际工作中根本无需考虑它的存在
- GIL作用面很窄 仅限于解释器级别,平时我们要想保证数据的安全应该自定义互斥锁(使用别人封装好的工具)
二、多线程与多进程
- 两个大前提:
CPU的个数:单个、多个 - 任务的类型
IO密集型
计算密集型
单个CPU
- 多个IO密集型任务
多进程:浪费资源 无法利用多个CPU
多线程:节省资源 切换+保存状态 - 多个计算密集型任务
多进程:耗时更长 创建进程的消耗+切换消耗
多线程:耗时较短 切换消耗
多个CPU
- 多个IO密集型任务
多进程:浪费资源 多个CPU无用武之地
多线程:节省资源 切换+保存状态 - 多个计算密集型任务
多进程:利用多核 速度更快
多线程:速度较慢
结论:
多进程和多线程都有具体的应用场景 尤其是多线程并不是没有用
代码验证
计算密集型
- 多进程
from multiprocessing import Process
import os
import time
def work():
res = 1
for i in range(1, 10000):
res *= i
if __name__ == '__main__':
print(os.cpu_count()) # 12 查看当前计算机CPU个数
start_time = time.time()
p_list = []
for i in range(12):
p = Process(target=work)
p.start()
p_list.append(p)
for p in p_list:
p.join()
print('总耗时:%s' % (time.time() - start_time))
- 多线程
from threading import Thread
import os
import time
def work():
res = 1
for i in range(1, 10000):
res *= i
if __name__ == '__main__':
print(os.cpu_count()) # 12 查看当前计算机CPU个数
start_time = time.time()
t_list = []
for i in range(12):
t = Thread(target=work)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print('总耗时:%s' % (time.time() - start_time))
- 结果:
多进程
0.035416197776794434
多线程
0.2477881908416748 - 结论
两者差了一个数量级(越多差距越大),多进程更好
IO密集型
- 多进程
from multiprocessing import Process
import time
def work():
time.sleep(2) # 模拟纯IO操作
if __name__ == '__main__':
start_time = time.time()
p_list = []
for i in range(100):
p = Process(target=work)
p.start()
p_list.append(p)
for p in p_list:
p.join()
print('总耗时:%s' % (time.time() - start_time))
- 多线程
from threading import Thread
import time
def work():
time.sleep(2) # 模拟纯IO操作
if __name__ == '__main__':
start_time = time.time()
t_list = []
for i in range(100):
t = Thread(target=work)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print('总耗时:%s' % (time.time() - start_time))
- 结果:
多线程
总耗时:2.037888288497925
多进程
总耗时:4.773791790008545 - 结论
两者差了两个数量级,多线程更好
三、死锁现象
死锁就是一个线程需要的锁在另外一个线程哪里,而自己手里拿着他需要的锁,一次死锁
代码模拟
from threading import Lock,Thread
mutex_a = Lock()
mutex_b = Lock()
class Mythread(Thread):
def run(self):
self.fun1()
self.fun2()
def fun1(self):
mutex_a.acquire()
print("get a")
mutex_b.acquire()
print("get b")
mutex_b.release()
mutex_a.release()
def fun2(self):
mutex_b.acquire()
print("get B")
import time
time.sleep(2)
mutex_a.acquire()
print("get A")
mutex_a.release()
mutex_b.release()
if __name__ == '__main__':
for i in range(8):
thread_test = Mythread()
thread_test.start()
结果分析
- 死锁产生原因 首先线程1 抢到A锁 其他抢不到等着,接着抢到B锁 其他任然抢A
- 接着释放B锁,别的进程任然抢A接着释放A锁,别的进程抢到A锁,线程1抢到B锁,然后睡了2秒
- 其他要抢B锁,B锁在线程1上,然后线程1要抢A锁,A锁在线程2上,索要之锁都在别的线程中,所以死锁
总结
锁不能轻易使用并且以后我们也不会在自己去处理锁都是用别人封装的工具
四、信号量与event事件(了解)
信号量
信号量可以理解为多把锁,同时允许多个线程来更改数据。而互斥锁同时只允许一个线程更改数据。
信号量在不同的知识体系中 展示出来的功能是不一样的
eg:
- 在并发编程中信号量意思是多把互斥锁
- 在django框架中信号量意思是达到某个条件自动触发特定功能
“”"
如果将自定义互斥锁比喻成是单个厕所(一个坑位)
那么信号量相当于是公共厕所(多个坑位)
“”"
from threading import Thread, Semaphore
import time
import random
sp = Semaphore(5) # 创建一个有五个坑位(带门的)的公共厕所
def task(name):
sp.acquire() # 抢锁
print('%s正在蹲坑' % name)
time.sleep(random.randint(1, 5))
sp.release() # 放锁
for i in range(1, 31):
t = Thread(target=task, args=('伞兵%s号' % i, ))
t.start()
event事件
Event对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行。
“”"
子线程的运行可以由其他子线程决定!!!
“”"
event的方法
- is_set()
当且仅当内部标志为True时返回True。 - set()
将内部标志设置为True。所有等待它成为True的线程都被唤醒。当标志保持在True的状态时,线程调用wait()是不会阻塞的。 - clear()
将内部标志重置为False。随后,调用wait()的线程将阻塞,直到另一个线程调用set()将内部标志重新设置为True。 - wait(timeout=None)
阻塞直到内部标志为真。如果内部标志在wait()方法调用时为True,则立即返回。否则,则阻塞,直到另一个线程调用set()将标志设置为True,或发生超时。
该方法总是返回True,除非设置了timeout并发生超时。
from threading import Thread, Event
import time
event = Event() # 类似于造了一个红绿灯
def light():
print('红灯亮着的 所有人都不能动')
time.sleep(3)
print('绿灯亮了 油门踩到底 给我冲!!!')
event.set()
def car(name):
print('%s正在等红灯' % name)
event.wait()
print('%s加油门 飙车了' % name)
t = Thread(target=light)
t.start()
for i in range(20):
t = Thread(target=car, args=('熊猫PRO%s' % i,))
t.start()
#这种效果其实也可以通过其他手段实现 比如队列(只不过没有event简便)
五、进程池与线程池
提交任务的两种方式
-
同步:提交完任务后,就在原地等待,直到该任务运行完拿到返回值后,才执行下一行代码—>导致任务的运行方式为串行
-
异步:提交完任务后,就立即执行下一行代码,当任务有返回值后,自动触发回调函数—>导致任务的运行方式为并发
实现并发的手段有两种:多线程和多进程。
注:并发是指多个任务看起来是同时运行的。主要是切换+保存状态。
什么是池
可以将其理解为一种容器,有其固定的大小
池的功能就是限制启动的进程数或线程数
什么时候用线程池/进程池
-
什么时候用池:当程序中的任务并发数远远大于计算机的承受能力时,就应该用池的概念将开启的进程数或者线程数限制在计算机的承受范围之内
-
用什么样的池:用进程池还是线程池取决于程序的类型,对于IO密集型—>线程,对于计算密集型—>进程
- concurent.future模块:
concurrent.futures模块提供了高度封装的异步调用接口
- ProcessPoolExecutor: 进程池,提供异步调用
p = ProcessPoolExecutor(max_works)对于进程池如果不写max_works:默认的是cpu的数目,默认是4个
- ThreadPoolExecutor:线程池,提供异步调用
p = ThreadPoolExecutor(max_works)对于线程池如果不写max_works:默认的是cpu的数目*5
进程池
import os,time
from concurrent.futures import ProcessPoolExecutor
def task():
sum = 0
print('%s正在产生数据...'%os.getpid())
for i in range(1000000):
sum +=i
return sum
def dispose(sum):
print('%s正在处理数据...'%os.getpid())
time.sleep(1) # 模拟处理数据
if __name__ == '__main__':
p = ProcessPoolExecutor(4)
for i in range(10): # 有十个任务需要并发处理,但是假设计算机无法承受开启10个进程
feture=p.submit(task) # 提交任务,并得到一个feture对象
feture.add_done_callback(dispose) # 为其绑定回调函数,并将feture对象作为参数传入,会在任务执行完后自动触发
p.shutdown() # 关闭进程池的入口,并等待任务执行结束
print('主',os.getpid())
分析
1.只开启了4个子进程,且执行结束后,仍然是这四个子进程干剩下的任务(谁空闲,谁就执行下一个)
2.是主进程在执行回调函数
线程池
import time,random
from concurrent.futures import ThreadPoolExecutor
from threading import current_thread
def task():
sum = 0
print('%s正在产生数据...' % current_thread().name)
time.sleep(random.random()) # 模拟IO行为
return sum
def dispose(sum):
print('%s正在处理数据...' % current_thread().name)
time.sleep(1) # 模拟处理数据
if __name__ == '__main__':
p = ThreadPoolExecutor(4)
for i in range(10):
feture = p.submit(task)
feture.add_done_callback(dispose)
p.shutdown()
print('主', current_thread().name)
分析
1.只开启了四个线程实现并发,且剩余的任务仍然是由这四个线程完成(谁有空,谁执行)
2.与进程池不同,回调函数是由开启的子线程完成(谁有空,谁执行)
六、协程
在单线程的情况下实现并发。
-
目的
遇到IO就切换就可以降低单线程的IO时间,从而最大限度地提升单线程的效率。 -
个人理解
对于操作系统而言之认识进程和线程,协程只是程序员自己定义的。
协程就是自己通过代码来检测程序的IO操作并自己处理 让CPU感觉不到IO的存在从而最大幅度的占用CPU
实现并发是让多个任务看起来同时运行(切换+保存状态),cpu在运行一个任务的时候,会在两种情况下去执行其他的任务,一种是遇到了I/O操作,一种是计算时间过长。其中第二种情况使用线程并发并不能提升效率,运算密集型的并发反而会降低效率。
代码实现
from gevent import monkey;monkey.patch_all() # 固定编写 用于检测所有的IO操作
from gevent import spawn
import time
def play(name):
print('%s play 1' % name)
time.sleep(5)
print('%s play 2' % name)
def eat(name):
print('%s eat 1' % name)
time.sleep(3)
print('%s eat 2' % name)
start_time = time.time()
g1 = spawn(play, 'jason')
g2 = spawn(eat, 'jason')
g1.join() # 等待检测任务执行完毕
g2.join() # 等待检测任务执行完毕
print('总耗时:', time.time() - start_time) # 正常串行肯定是8s+
# 5.039459228515625 代码控制切换
本文概述了GIL与普通互斥锁的区别,探讨了多线程与多进程在单/多CPU环境下的应用,剖析了死锁现象及信号量与event事件,讲解了进程池与线程池的实战运用,以及协程在单线程中的高效并发实践。
530

被折叠的 条评论
为什么被折叠?



