今日内容概念
- GIL与普通互斥锁区别
- 验证多线程是否有用
- 死锁现象(强调锁不能轻易使用)
- 信号量与event事件(了解)
- 进程池与线程池(重点)
- 协程
GIL与普通互斥锁区别
互斥锁是运用在一个py文件中的,也就是在一个应用程序中,是代码层面的锁
其实,Python解释器也是一个应用程序。只是说这个应用程序不是我们实现的,我们自己的python程序都要运行在解释器之上,这个应用程序被用来帮我们运行我们自己的程序
线程互斥锁是Python代码层面的锁,解决我们自己写的Python程序中多线程共享资源的问题
GIL是Python解释器层面的锁,解决解释器中多个线程的竞争资源问题
# 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)
# 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()
"""
抢锁放锁也有简便写法(with上下文管理)
with mutex:
pass
"""
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)
验证多线程作用
"""
两个大前提
CPU的个数
单个
多个
任务的类型
IO密集型
计算密集型
"""
# 单个CPU
多个IO密集型任务
多进程:浪费资源 无法利用多个CPU
多线程:节省资源 切换+保存状态
多个计算密集型任务
多进程:耗时更长 创建进程的消耗+切换消耗
多线程:耗时较短 切换消耗
# 多个CPU
多个IO密集型任务
多进程:浪费资源 多个CPU无用武之地
多线程:节省资源 切换+保存状态
多个计算密集型任务
多进程:利用多核 速度更快
多线程:速度较慢
结论:多进程和多线程都有具体的应用场景 尤其是多线程并不是说它没有一点用
多进程: 好处是方便管理各自的事,各干各的,互不干涉,一个线程死了,整个进程就崩溃了,但是,其他进程不会崩溃
这如果在一个大系统全是线程的,只有一个线程死了,影响整个系统,那就完蛋了
例子: 好比开飞机,飞机有两个发动机,坏了一个,其他功能还能支持飞机运行,就不会掉下来挂掉,如果只有一个发动机,那就直接Gameover了
'''代码验证'''
from threading import Thread
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()
# 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.08273792266845703
多线程
0.28725099563598633
两者差了一个数量级(越多差距越大)
结论
多进程更好
"""
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()
# for t in t_list:
# t.join()
p_list = []
for i in range(100):
p = Process(target=work)
p.start()
for p in p_list:
p.join()
print('总耗时:%s' % (time.time() - start_time))
"""
IO密集型
多线程
总耗时:0.007348060607910156
多进程
总耗时:0.1564030647277832
两者差了两个数量级
结论
多线程更好
"""
死锁现象
# 锁就算掌握了如何抢 如何放 也会产生死锁现象
# 死锁的概念
死锁: 一直等待对方释放锁的情景就是死锁
两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,程序无法推进
比如: 现实社会中,男女双方一直等待对方先道歉的这种行为就好比是死锁
# 造成死锁的原因
1、在多线程程序中,死锁问题很大一部分是由于线程同时获取多个锁造成的
2、在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁
3、尽管死锁很少发生,但一旦发生就会造成应用的停止响应
# 死锁的结果
会造成应用程序的停止响应,不能再处理其它任务了
# 死锁案例
import threading
import time
# 创建互斥锁
lock = threading.Lock()
#根据下标去取值, 保证同一时刻只能有一个线程去取值
def get_value(index):
# 上锁
lock.acquire()
print(threading.current_thread())
my_list = [3,6,8,1]
# 判断下标释放越界
if index >= len(my_list):
print("下标越界:", index)
return
value = my_list[index]
print(value)
time.sleep(0.2)
# 释放锁
lock.release()
if __name__ == '__main__':
# 模拟大量线程去执行取值操作
for i in range(30):
sub_thread = threading.Thread(target=get_value, args=(i,))
sub_thread.start()
结果:'''
3
6
8
1
下标越界: 4
然后就会一直停在这里,造成锁死
'''
# 在合适的地方释放锁
import threading
import time
#创建互斥锁
lock = threading.Lock()
#根据下标去取值, 保证同一时刻只能有一个线程去取值
def get_value(index):
# 上锁
lock.acquire()
print(threading.current_thread())
my_list = [3,6,8,1]
if index >= len(my_list):
print("下标越界:", index)
# 当下标越界需要释放锁,让后面的线程还可以取值
lock.release()
return
value = my_list[index]
print(value)
time.sleep(0.2)
# 释放锁
lock.release()
if __name__ == '__main__':
# 模拟大量线程去执行取值操作
for i in range(30):
sub_thread = threading.Thread(target=get_value, args=(i,))
sub_thread.start()
结果:'''
3
6
8
1
下标越界: 4
......一直到最后的下标都不会锁住不放
下标越界: 29
'''
信号量(了解)
信号量在不同的知识体系中 展示出来的功能是不一样的
eg:
在并发编程中信号量意思是多把互斥锁
在django框架中信号量意思是达到某个条件自动触发特定功能
# 信号量
1.是一个变量,控制着对公共资源或者临界区的访问。信号量维护着一个计数器,指定可同时访问资源或者进入临界区的线程数。
2.每次有一个线程获得信号量时,计数器-1。若计数器为0,其他线程就停止访问信号量,直到另一个线程释放信号量
线程锁(互斥锁)同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去
"""
如果将自定义互斥锁比喻成是单个厕所(一个坑位)
那么信号量相当于是公共厕所(多个坑位)
"""
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=threading.Event() #设置一个事件实例
event.set() #设置标志位
event.clear() #清空标志位
event.wait() #等待设置标志位
event.isSet() #判断标志位是否清空
"""
子线程的运行可以由其他子线程决定!!!
"""
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简便)
'''
红灯: 所有人肯定是按顺序停车的
绿灯: 车启动有快慢
'''
进程池与线程池(重点)
concurrent.futures下面主要有俩接口
ThreadPoolExecutor 线程池
ProcessPoolExecutor 进程池
"""
补充:
服务端必备的三要素
1.24小时不间断提供服务
2.固定的ip和port
3.支持高并发
回顾:
TCP服务端实现并发
多进程:来一个客户端就开一个进程(临时工)
多线程:来一个客户端就开一个线程(临时工)
问题:
计算机硬件是有物理极限的 我们不可能无限制的创建进程和线程
措施:
池:
保证计算机硬件安全的情况下提升程序的运行效率
进程池:
提前创建好固定数量的进程 后续反复使用这些进程(合同工)
线程池:
提前创建好固定数量的线程 后续反复使用这些线程(合同工)
如果任务超出了池子里面的最大进程或线程数 则原地等待
强调:
进程池和线程池其实降低了程序的运行效率 但是保证了硬件的安全!!!
"""
# 代码演示(掌握)
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
# 线程池
pool = ThreadPoolExecutor(5) # 线程池线程数默认是CPU个数的五倍 也可以自定义
'''上面的代码执行之后就会立刻创建五个等待工作的线程'''
'''不应该自己主动等待结果 应该让异步提交自动提醒>>>:异步回调机制'''
pool.submit(task, i).add_done_callback(func)
"""add_done_callback只要任务有结果了 就会自动调用括号内的函数处理"""
# 进程池
pool = ProcessPoolExecutor(5) # 进程池进程数默认是CPU个数 也可以自定义
'''上面的代码执行之后就会立刻创建五个等待工作的进程'''
pool.submit(task, i).add_done_callback(func)
协程
'''
进程:资源单位
线程:执行单位
协程:单线程下实现并发
并发的概念:切换+保存状态
对于操作系统而言之认识进程和线程
协程就是自己通过代码来检测程序的IO操作并自己处理 让CPU感觉不到IO的存在从而最大幅度的占用CPU
类似于一个人同时干接待和服务客人的活 在接待与服务之间来回切换
'''
并发,很容易想到的是多线程,其实协程也是实现并发的一种方式,只不过协程是单线程
# 协程与线程的差异
在实现多任务时, 线程切换从系统层面远不止保存和恢复CPU上下文这么简单。操作系统为了程序运行的高效性,每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作,所以线程的切换非常耗性能
但是协程的切换只是单纯地操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住
# 基本使用
# 保存的功能 我们其实接触过 yield 但是无法做到检测IO切换
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.00609827041626 代码控制切换
基于协程实现TCP服务端并发
from gevent import monkey;monkey.patch_all()
from gevent import spawn
import socket
def communication(sock):
while True:
data = sock.recv(1024) # IO操作
print(data.decode('utf8'))
sock.send(data.upper())
def get_server():
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
sock, addr = server.accept() # IO操作
spawn(communication, sock)
g1 = spawn(get_server)
g1.join()
'''
终极结论
python可以通过开设多进程 在多进程下开设多线程 在多线程使用协程
从而让程序执行的效率达到极致
'''