python 并发编程
一 并发编程相关概念
并发编程
什么是并发编程
并发指的是多个任务同时被执行,并发编程指的是编写支持多任务的应用程序
1串行:自上而下顺序执行
2并发:多个任务同时执行,但是本质上是在不同进程间切换执行,由于速度快所以感觉是同时进行的
3并行:是真正的同时进行,必须具备的是多核CPU,有几个核心就能并行几个任务,当任务数量超过核心数,任务进行并发
遇到的状态:
阻塞和非阻塞可以用来描述执行任务的方式
1阻塞:程序遇到了IO操作,无法继续执行代码一种
- input()默认是一个阻塞操作
2非阻塞:程序没有遇到IO操作的一种
- 我们可以用一些手段将阻塞的操作变成非阻塞的操作,非阻塞的socket
一个进程的三种状态:1阻塞,2运行,3就绪
学习并发的目的
编写可以同时执行多个任务的程序,来提高效率
串行和并发都是程序处理任务的方式
多道技术
实现原理:---------------------有了多道技术,计算机就可以同时并发处理多个任务
1,空间复用:
同一时间,加载多个任务到内存中,多个进程之间内存区域需要相互隔离,这个隔离是物理层面的隔离,目的是保证数据的安全
2,时间的复用:
操作系统会在多个进程之间按做切换执行,切换任务的两个情况
- 1当一个进程遇到了IO操作,会自动切换
- 2当一个任务执行时间超过阈值会强制i切换
在切换前必须保存状态,以便恢复执行----频繁切换是需要消耗资源的
当所有任务都没有io操作时,切换执行效率反而降低,为了保证并发执行,必须牺牲效率
同步和异步
"""描述的是任务的提交方式"""
同步:任务提交之后,原地等待任务的返回结果,等待的过程中不做任何事(干等)
程序层面上表现出来的感觉就是卡住了
异步:任务提交之后,不原地等待任务的返回结果,直接去做其他事情
我提交的任务结果如何获取?
任务的返回结果会有一个异步回调机制自动处理
阻塞非阻塞
"""描述的程序的运行状态"""
阻塞:阻塞态
非阻塞:就绪态、运行态
理想状态:我们应该让我们的写的代码永远处于就绪态和运行态之间切换
二 Python 开启多进程
进程概念
什么是进程
进程就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
1,进程指的是正在运行的程序,是一系列过程的统称,也是操作系统进行资源分配的基本单位
2,进程怎么来的:当程序从硬盘读入内存,进程就产生了
3,多进程的概念:多个程序同一时间被装入内存并且执行(一个程序可以产生多个进程,比如说运行多个qq程序)
进程是实现并发的一种方式-------涉及到操作系统,因为这个概念来自于操作系统,没有操作系统就没有进程
- 多进程实现原理就是操作系统调度进程的原理
进程的创建和销毁
创建:
- 用户的交互式请求,鼠标双击
- 由一个正在运行的程序,调用了开启进程的接口,例如 subprosess
- 一个批作业的开始
- 系统的初始化
销毁:
- 任务完成,自愿退出
- 强制退出,taskkill kill(非自愿)
- 程序遇到了异常
- 严重的的错误,访问了不该访问的内存
查看进程
PID是当前进程的编号
PPID是父进程的编号
在运行py文件时其实是运行的python解释器,一个python进程就是一个独立的解释器
访问PID PPID
import os
os.getpid()
os.getppid()
父进程和子进程
一个程序的运行就是一个进程,该程序运行过程中又通过系统调用创建了一个进程,被创建的进程叫子进程,主程序就叫父进程
进程调度
先来先服务调度算法
短作业优先调度算法
时间片轮转法+多级反馈队列
进程运行的三状态
就绪态:程序等待被运行
运行态:程序以及在运行
阻塞态:程序由于io操作,运行中断,Io结束后程序进入就绪态
开启方法
# 第一种方式 Process类
from multiprocessing import Process
import time
def task(name):
print('%s is running'%name)
time.sleep(3)
print('%s is over'%name)
#Linux创建进程会copy主进程代码去执行,windows创建子进程以导入模块的方式执行代码,为了避免重复导入的问题,创建进程的代码写在main下
if __name__ == '__main__':
# 1 创建一个对象,target=task开启的线程要执行的函数,args=('jason',)调用函数需要传的参数位置参数只接受元组格式,关键字传参接受字典如:kwargs{key:value}
p = Process(target=task, args=('jason',))
# 2 调用对象开启线程的方法,让系统再开启一个进程执行传入的函数
p.start()
print('主')
# 第二种方式 继承Process对象来重写run方法创建进程
from multiprocessing import Process
import time
class MyProcess(Process):
def run(self):
print('hello bf girl')
time.sleep(1)
print('get out!')
if __name__ == '__main__':
p = MyProcess()
p.start()
print('主')
join的使用
join是让主进程等待该子进程运行结束之后,再继续运行。不影响其他子进程的执行
from multiprocessing import Process
import time
def task(name, n):
print('%s is running'%name)
time.sleep(n)
print('%s is over'%name)
if __name__ == '__main__':
start_time = time.time()
p_list = []
for i in range(1, 4):
p = Process(target=task, args=('子进程%s'%i, i))
p.start()
p_list.append(p)
for p in p_list:
p.join()
print('主', time.time() - start_time)
进程数据隔离
from multiprocessing import Process
money = 100
def task():
global money # 局部修改全局
money = 666
print('子',money)
if __name__ == '__main__':
p = Process(target=task)
p.start()
p.join()
print(money)
进程对象其他方法
"""
一台计算机上面运行着很多进程,那么计算机是如何区分并管理这些进程服务端的呢?
计算机会给每一个运行的进程分配一个PID号
windows电脑
进入cmd输入tasklist即可查看
tasklist |findstr PID查看具体的进程
mac电脑
进入终端之后输入ps aux
ps aux|grep PID查看具体的进程
"""
from multiprocessing import Process, current_process
current_process().pid # 查看当前进程的进程号
import os
os.getpid() # 查看当前进程进程号
os.getppid() # 查看当前进程的父进程进程号
p.terminate() # 杀死当前进程
# 是告诉操作系统帮你去杀死当前进程 但是需要一定的时间 而代码的运行速度极快
time.sleep(0.1)
print(p.is_alive()) # 判断当前进程是否存活
进程相关的名词
僵尸进程与孤儿进程
# 僵尸进程
死了但是没有死透
当你开设了子进程之后 该进程死后不会立刻释放占用的进程号
因为我要让父进程能够查看到它开设的子进程的一些基本信息 占用的pid号 运行时间。。。
所有的进程都会步入僵尸进程
# 孤儿进程
子进程存活,父进程意外死亡
操作系统会开设一个“儿童福利院”专门管理孤儿进程回收相关资源
**守护进程 **daemon
守护进程是一种进程,它会在它的主进程结束后也立即结束
from multiprocessing import Process
import time
def task(name):
print('%s总管正在活着'% name)
time.sleep(3)
print('%s总管正在死亡' % name)
if __name__ == '__main__':
p = Process(target=task,args=('egon',))
# p = Process(target=task,kwargs={'name':'egon'})
p.daemon = True # 将进程p设置成守护进程 这一句一定要放在start方法上面才有效否则会直接报错
p.start()
print('皇帝jason寿终正寝')
互斥锁
因为多个进程之间数据是隔离的,所以多个进程操作同一份数据的时候,会出现数据错乱的问题
针对上述问题,解决方式就是加锁处理:将并发变成串行,虽然牺牲效率但是保证了数据的安全
from multiprocessing import Process, Lock
import json
import time
import random
# 查票
def search(i):
# 文件操作读取票数
with open('data','r',encoding='utf8') as f:
dic = json.load(f)
print('用户%s查询余票:%s'%(i, dic.get('ticket_num')))
# 字典取值不要用[]的形式 推荐使用get 你写的代码打死都不能报错!!!
# 买票 1.先查 2.再买
def buy(i):
# 先查票
with open('data','r',encoding='utf8') as f:
dic = json.load(f)
# 模拟网络延迟
time.sleep(random.randint(1,3))
# 判断当前是否有票
if dic.get('ticket_num') > 0:
# 修改数据库 买票
dic['ticket_num'] -= 1
# 写入数据库
with open('data','w',encoding='utf8') as f:
json.dump(dic,f)
print('用户%s买票成功'%i)
else:
print('用户%s买票失败'%i)
# 整合上面两个函数
def run(i, mutex):
search(i)
# 给买票环节加锁处理
# 抢锁
mutex.acquire()
buy(i)
# 释放锁
mutex.release()
if __name__ == '__main__':
# 在主进程中生成一把锁 让所有的子进程抢 谁先抢到谁先买票
mutex = Lock()
for i in range(1,11):
p = Process(target=run, args=(i, mutex))
p.start()
"""
扩展 行锁 表锁
注意:
1.锁不要轻易的使用,容易造成死锁现象(我们写代码一般不会用到,都是内部封装好的)
2.锁只在处理数据的部分加来保证数据安全(只在争抢数据的环节加锁处理即可)
"""
进程通信 IPC
- 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
以上几种进程间通信方式中,消息队列是使用的比较频繁的方式。
Queue模块使用
"""
队列:管道+锁
队列:先进先出
堆栈:先进后出
"""
from multiprocessing import Queue
# 创建一个队列
q = Queue(5) # 括号内可以传数字 标示生成的队列最大可以同时存放的数据量,默认32217个
# 往队列中存数据
q.put(111) # 当队列数据放满了之后 如果还有数据要放程序会阻塞 直到有位置让出来 不会报错
q.full() # 判断当前队列是否满了
q.empty() # 判断当前队列是否空了
v5 = q.get() # 去队列中取数据,队列中如果已经没有数据的话 get方法会原地阻塞
V6 = q.get_nowait() # 没有数据直接报错queue.Empty
v7 = q.get(timeout=3) # 没有数据之后原地等待三秒之后再报错 queue.Empty
try:
v6 = q.get(timeout=3)
print(v6)
except Exception as e:
print('没有了!')
"""
q.full()
q.empty()
q.get_nowait()
在多进程的情况下是不精确
"""
生产者消费者模型
"""
生产者:生产/制造东西的
消费者:消费/处理东西的
该模型除了上述两个之外还需要一个媒介
生活中的例子做包子的将包子做好后放在蒸笼(媒介)里面,买包子的取蒸笼里面拿
厨师做菜做完之后用盘子装着给你消费者端过去
生产者和消费者之间不是直接做交互的,而是借助于媒介做交互
生产者(做包子的) + 消息队列(蒸笼) + 消费者(吃包子的)
"""
from multiprocessing import Process, Queue, JoinableQueue
import time
import random
def producer(name,food,q):
for i in range(5):
data = '%s生产了%s%s'%(name,food,i)
# 模拟延迟
time.sleep(random.randint(1,3))
print(data)
# 将数据放入 队列中
q.put(data)
def consumer(name,q):
# 消费者胃口很大 光盘行动
while True:
food = q.get() # 没有数据就会卡住
# 判断当前是否有结束的标识
# if food is None:break
time.sleep(random.randint(1,3))
print('%s吃了%s'%(name,food))
q.task_done() # 告诉队列你已经从里面取出了一个数据并且处理完毕了
if __name__ == '__main__':
q = JoinableQueue()
"""
JoinableQueue 每当你往该队列中存入数据的时候 内部会有一个计数器+1
没当你调用task_done的时候 计数器-1
q.join() 当计数器为0的时候 才往后运行
"""
p1 = Process(target=producer,args=('大厨egon','包子',q))
p2 = Process(target=producer,args=('马叉虫tank','泔水',q))
c1 = Process(target=consumer,args=('春哥',q))
c2 = Process(target=consumer,args=('新哥',q))
#生成者启动
p1.start()
p2.start()
# 将消费者设置成守护进程
c1.daemon = True
c2.daemon = True
#消费者启动
c1.start()
c2.start()
#等待消费者进程结束,程序才往下运行
p1.join()
p2.join()
#消费者取不到数据时会被卡主,主进程结束时它就成了孤儿进程
q.join() # 用q.join配合守护进程解决这个问题
#q.join等待队列中所有的数据被取完再执行往下执行代码
#只要q.join执行完毕 说明消费者已经处理完数据了 消费者就没有存在的必要了
# 所以可以将消费者提前设置成守护进程
三 python 开启多线程
线程的概念
"""
进程:资源单位
线程:执行单位
将操作系统比喻成一个大的工厂
那么进程就相当于工厂里面的车间
而线程就是车间里面的流水线
每一个进程肯定自带一个线程
再次总结:
进程:资源单位(起一个进程仅仅只是在内存空间中开辟一块独立的空间)
线程:执行单位(真正被cpu执行的其实是进程里面的线程,线程指的就是代码的执行过程,执行代码中所需要使用到的资源都找所在的进程索要)
进程和线程都是虚拟单位,只是为了我们更加方便的描述问题
"""
线程的作用
"""
开设进程
1.申请内存空间 耗资源
2.“拷贝代码” 耗资源
开线程
一个进程内可以开设多个线程,在用一个进程内开设多个线程无需再次申请内存空间操作
总结:
开设线程的开销要远远的小于进程的开销
同一个进程下的多个线程数据是共享的!!!
"""
开启方法
线程的基本语法
线程的开设方式,语法都与进程相同
都为两种: 1 导入Thread进而实例化(传入target对象) 2 继承Thread自定义类,类中定义run函数,添加代码
from multiprocessing import Process
from threading import Thread
import time
def task(name):
print('%s is running'%name)
time.sleep(1)
print('%s is over'%name)
# 开启线程不需要在main下面执行代码 直接书写就可以
# 但是我们还是习惯性的将启动命令写在main下面
t = Thread(target=task,args=('egon',))
# p = Process(target=task,args=('jason',))
# p.start()
t.start() # 创建线程的开销非常小 几乎是代码一执行线程就已经创建了
print('主')
from threading import Thread
import time
class MyThead(Thread):
def __init__(self, name):
"""针对刷个下划线开头双下滑线结尾(__init__)的方法 统一读成 双下init"""
# 重写了别人的方法 又不知道别人的方法里有啥 你就调用父类的方法
super().__init__()
self.name = name
def run(self):
print('%s is running'%self.name)
time.sleep(1)
print('egon DSB')
if __name__ == '__main__':
t = MyThead('egon')
t.start()
print('主')
线程对象的join方法
join方法与进程相同,都是等待进程或线程结束再继续执行后面代码
from threading import Thread
import time
def task(name):
print('%s is running'%name)
time.sleep(3)
print('%s is over'%name)
if __name__ == '__main__':
t = Thread(target=task,args=('egon',))
t.start()
t.join() # 主线程等待子线程运行结束再执行
print('主')
守护线程
主线程运行结束之后不会立刻结束 会等待所有其他非守护线程结束才会结束(注意线程结束与线程join的区别)
方法为: 在线程开启前 执行 线程对象.daemon = True
from threading import Thread
import time
def task(name):
print('%s is running'%name)
time.sleep(1)
print('%s is over'%name)
if __name__ == '__main__':
t = Thread(target=task,args=('egon',))
t.daemon = True
t.start()
print('主')
"""
主线程运行结束之后不会立刻结束 会等待所有其他非守护线程结束才会结束
因为主线程的结束意味着所在的进程的结束
"""
# 稍微有一点迷惑性的例子
from threading import Thread
import time
def foo():
print(123)
time.sleep(1)
print('end123')
def func():
print(456)
time.sleep(3)
print('end456')
if __name__ == '__main__':
t1 = Thread(target=foo)
t2 = Thread(target=func)
t1.daemon = True
t1.start()
t2.start()
print('主.......')
# 123 456 主....... end123 end456
线程数据共享
进程就像车间,线程就像流水线,对于同一车间下的不同流水线,他们的资源都在同一个车间内
所以进程是资源单位,而线程是执行单位,统一进程下的线程是数据共享的
from threading import Thread
import time
money = 100
def task():
global money
money = 666
print(money)
if __name__ == '__main__':
t = Thread(target=task)
t.start()
t.join()
print(money)
线程对象属性及其他方法
from threading import Thread, active_count, current_thread
import os,time
def task(n):
# print('hello world',os.getpid())
print('hello world',current_thread().name)
time.sleep(n)
if __name__ == '__main__':
t = Thread(target=task,args=(1,))
t1 = Thread(target=task,args=(2,))
t.start() # hello world Thread-1
t1.start() # hello world Thread-2
t.join()
Process finished with exit code 0
print('主',active_count()) # 统计当前正在活跃的线程数 主 2
# print('主',os.getpid()) # 同一进程下的不同线程,查看pid都相同 主 21196
# print('主',current_thread().name) # 获取线程名字 主 MainThread
TCP服务端实现并发的效果
多线程实现
import socket
from threading import Thread
from multiprocessing import Process
"""
服务端
1.要有固定的IP和PORT
2.24小时不间断提供服务
3.能够支持并发
从现在开始要养成一个看源码的习惯
我们前期要立志称为拷贝忍者 卡卡西 不需要有任何的创新
等你拷贝到一定程度了 就可以开发自己的思想了
"""
server =socket.socket() # 括号内不加参数默认就是TCP协议
server.bind(('127.0.0.1',8080))
server.listen(5)
# 将服务的代码单独封装成一个函数
def talk(conn):
# 通信循环
while True:
try:
data = conn.recv(1024)
# 针对mac linux 客户端断开链接后
if len(data) == 0: break
print(data.decode('utf-8'))
conn.send(data.upper())
except ConnectionResetError as e:
print(e)
break
conn.close()
# 链接循环
while True:
conn, addr = server.accept() # 接客
# 叫其他人来服务客户
# t = Thread(target=talk,args=(conn,))
t = Process(target=talk,args=(conn,))
t.start()
"""客户端"""
import socket
client = socket.socket()
client.connect(('127.0.0.1',8080))
while True:
client.send(b'hello world')
data = client.recv(1024)
print(data.decode('utf-8'))
线程互斥锁
1 实例化产生锁对象
2 锁对象.acquire() 进行抢锁操作
3 锁对象.release() 释放锁,让其他线程抢锁
4 使用 with mutex 能实现自动枪锁与释放锁的功能
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 mutex:
tmp = money
time.sleep(0.1)
money = tmp - 1
"""
if __name__ == '__main__':
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(money)
GIL全局解释器锁(重点)
1.GIL不是python的特点而是CPython解释器的特点
2.GIL是保证解释器级别的数据的安全
3.GIL会导致同一个进程下的多个线程的无法同时执行即无法利用多核优势(******)
4.针对不同的数据还是需要加不同的锁处理
5.解释型语言的通病:同一个进程下多个线程无法利用多核优势
"""
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)
"""
"""
python解释器其实有多个版本
Cpython
Jpython
Pypypython
但是普遍使用的都是CPython解释器
在CPython解释器中GIL是一把互斥锁,用来阻止同一个进程下的多个线程的同时执行
同一个进程下的多个线程无法利用多核优势!!!
疑问:python的多线程是不是一点用都没有???无法利用多核优势
因为cpython中的内存管理不是线程安全的
内存管理(垃圾回收机制)
1.应用计数
2.标记清楚
3.分代回收
"""
"""
重点:
1.GIL不是python的特点而是CPython解释器的特点
2.GIL是保证解释器级别的数据的安全
3.GIL会导致同一个进程下的多个线程的无法同时执行即无法利用多核优势(******)
4.针对不同的数据还是需要加不同的锁处理
5.解释型语言的通病:同一个进程下多个线程无法利用多核优势
"""
GIL与普通互斥锁的区别
from threading import Thread,Lock
import time
mutex = Lock()
money = 100
def task():
global money
# with mutex:
# tmp = money
# time.sleep(0.1)
# money = tmp -1
mutex.acquire()
tmp = money
time.sleep(0.1) # 只要你进入IO了 GIL会自动释放
money = tmp - 1
mutex.release()
if __name__ == '__main__':
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(money)
"""
100个线程起起来之后 要先去抢GIL
我进入io GIL自动释放 但是我手上还有一个自己的互斥锁
其他线程虽然抢到了GIL但是抢不到互斥锁
最终GIL还是回到你的手上 你去操作数据
"""
同一个进程下的多线程无法利用多核优势,是不是就没有用了
结论:
计算密集型:抢到GIL就能一直干活,但是一个进程一次只有一个GIL,所以无法利用多核优势
IO密集型:抢到GIL大部分时间都在IO不需要一直干活,所以利不利用多核优势影响不大
"""
多线程是否有用要看具体情况
单核:四个任务(IO密集型\计算密集型)
多核:四个任务(IO密集型\计算密集型)
"""
# 计算密集型 每个任务都需要10s
多核
多进程:总耗时 10+
多线程:总耗时 40+
# IO密集型
多核
多进程:相对浪费资源
多线程:更加节省资源
四 进程与线程通用补充
进程池与线程池(重点)
基本使用
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
import os
# pool = ThreadPoolExecutor(5) # 池子里面固定只有五个线程
# 括号内可以传数字 不传的话默认会开设当前计算机cpu个数五倍的线程
pool = ProcessPoolExecutor(5)
# 括号内可以传数字 不传的话默认会开设当前计算机cpu个数进程
"""
池子造出来之后 里面会固定存在五个线程
这个五个线程不会出现重复创建和销毁的过程
池子造出来之后 里面会固定的几个进程
这个几个进程不会出现重复创建和销毁的过程
池子的使用非常的简单
你只需要将需要做的任务往池子中提交即可 自动会有人来服务你
"""
def task(n):
print(n,os.getpid())
time.sleep(2)
return n**n
def call_back(res_obj):
print('call_back>>>:',res_obj.result())
"""
任务的提交方式
同步:提交任务之后原地等待任务的返回结果 期间不做任何事
异步:提交任务之后不等待任务的返回结果 执行继续往下执行
返回结果如何获取???
异步提交任务的返回结果 应该通过回调机制来获取
回调机制
就相当于给每个异步任务绑定了一个定时炸弹
一旦该任务有结果立刻触发爆炸
"""
if __name__ == '__main__':
for i in range(20): # 朝池子中提交20个任务
res = pool.submit(task, i).add_done_callback(call_back)
死锁与递归锁
死锁:死锁是一种现象,并不是一把锁
虽然了解了锁的使用:抢锁必须要释放锁,但是其实在操作锁的时候也极其容易产生死锁现象
(整个程序卡死 阻塞)
如:每个线程都同时需要两个锁才能完全运行,但是出现了两个线程分别拿着其中一把锁,就产生了死锁现象
from threading import Thread, Lock
import time
mutexA = Lock()
mutexB = Lock()
# 类只要加括号多次 产生的肯定是不同的对象
# 如果想要实现多次加括号等到的是相同的对象 ---> 单例模式
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()
递归锁
递归锁:可以用来解决死锁现象的一把锁
from threading import RLock
"""
递归锁的特点
可以被连续的acquire和release
但是只能被第一个抢到这把锁执行上述操作
它的内部有一个计数器 每acquire一次计数加一 每realse一次计数减一
只要计数不为0 那么其他人都无法抢到该锁
"""
# 将上述的
mutexA = Lock()
mutexB = Lock()
# 类只要加括号多次 产生的肯定是不同的对象
# 如果想要实现多次加括号等到的是相同的对象 ---> 单例模式
# 换成
mutexA = mutexB = RLock()
信号量
信号量在不同的阶段可能对应不同的技术点
在并发编程中信号量指的是一种锁 !!!
特点是这把锁可以分成多把锁
"""
如果我们将互斥锁比喻成一个厕所的话
那么信号量就相当于多个厕所
"""
from threading import Thread, Semaphore
import time
import random
sm = Semaphore(5) # 括号内写数字 写几就表示开设几个坑位
def task(name):
sm.acquire()
print('%s 正在蹲坑'% name)
time.sleep(random.randint(1, 5))
sm.release()
if __name__ == '__main__':
for i in range(20):
t = Thread(target=task, args=('伞兵%s号'%i, ))
t.start()
# 第一批能进入5个人,后面每走一个人就能重新进一个人
Event事件
一些进程/线程需要等待另外一些进程/线程运行完毕之后才能运行
event可以实现类似于发射信号一样的效果,使执行到event.wait()的线程挂起,直到出现了event.set()才继续执行
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)
if __name__ == '__main__':
t = Thread(target=light)
t.start()
for i in range(20):
t = Thread(target=car, args=('%s'%i, ))
t.start()
线程队列
"""
同一个进程下多个线程数据是共享的
为什么先同一个进程下还会去使用队列呢
因为队列是
管道 + 锁
所以用队列还是为了保证数据的安全
"""
# 我们现在使用的队列都是只能在本地测试使用
# 后期使用redis等封装完成的队列
队列q: 先进先出
import queue
q = queue.Queue(3)
q.put(1)
q.get()
q.get_nowait()
q.get(timeout=3)
q.full()
q.empty()
堆栈q: 后进先出
import queue
q = queue.LifoQueue(3) # last in first out
q.put(1)
q.put(2)
q.put(3)
print(q.get()) # 3
优先级q: 指定谁先出
可以给放入队列中的数据设置进出的优先级
lowest first 数字越小优先级越高
import queue
q = queue.PriorityQueue(4)
q.put((10, '111'))
q.put((100, '222'))
q.put((0, '333'))
q.put((-5, '444'))
print(q.get()) # (-5, '444')
# put括号内放一个元祖 第一个放数字表示优先级
# 需要注意的是 数字越小优先级越高!!!
五 协程
"""
进程:资源单位
线程:执行单位
协程:这个概念完全是程序员自己创造出来的
作用:单线程下实现并发
原理:我们程序员自己再代码层面上检测我们所有的IO操作
一旦遇到IO了 我们在代码级别完成切换
这样给CPU的感觉是你这个程序一直在运行 没有IO
从而提升程序的运行效率
多道技术
切换+保存状态
CPU两种切换
1.程序遇到IO
2.程序长时间占用
TCP服务端
accept
recv
代码如何做到
切换+保存状态
切换
切换不一定是提升效率 也有可能是降低效率
IO切 提升
没有IO切 降低
保存状态
保存上一次我执行的状态 下一次来接着上一次的操作继续往后执行
yield
"""
携程的实现:gevent模块
(安装gevent模块)
python -m pip --default-timeout=100 install gevent -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
from gevent import monkey;monkey.patch_all()
from gevent import spawn
import time
"""
gevent模块本身无法检测常见的一些io操作,需要使用猴子补丁
在使用的时候需要你额外的导入一句话
from gevent import monkey;monkey.patch_all()
"""
def heng():
print('哼')
time.sleep(2)
print('哼')
def ha():
print('哈')
time.sleep(3)
print('哈')
def heiheihei():
print('heiheihei')
time.sleep(5)
print('heiheihei')
start_time = time.time()
heng()
ha()
heiheihei()
print(time.time() - start_time) # 10.007060527801514
start_time = time.time()
g1 = spawn(heng) #spawn会自动检测io然后切换代码
g2 = spawn(ha)
g3 = spawn(heiheihei)
g1.join()
g2.join() # 等待被检测的任务执行完毕 再往后继续执行
g3.join()
print(time.time() - start_time) # 5.005439043045044
协程实现TCP服务端的并发
# 服务端
from gevent import monkey;monkey.patch_all()
import socket
from gevent import spawn
def communication(conn):
while True:
try:
data = conn.recv(1024)
if len(data) == 0: break
conn.send(data.upper())
except ConnectionResetError as e:
print(e)
break
conn.close()
def server(ip, port):
server = socket.socket()
server.bind((ip, port))
server.listen(5)
while True:
conn, addr = server.accept()
spawn(communication, conn)
if __name__ == '__main__':
g1 = spawn(server, '127.0.0.1', 8080)
g1.join()
# 客户端
from threading import Thread, current_thread
import socket
def x_client():
client = socket.socket()
client.connect(('127.0.0.1',8080))
n = 0
while True:
msg = '%s say hello %s'%(current_thread().name,n)
n += 1
client.send(msg.encode('utf-8'))
data = client.recv(1024)
print(data.decode('utf-8'))
if __name__ == '__main__':
for i in range(500):
t = Thread(target=x_client)
t.start()
六 5大网络IO模型
IO模型简介
"""
我们这里研究的IO模型都是针对网络IO的
Stevens在文章中一共比较了五种IO Model:
* blocking IO 阻塞IO
* nonblocking IO 非阻塞IO
* IO multiplexing IO多路复用
* signal driven IO 信号驱动IO
* asynchronous IO 异步IO
由于signal driven IO(信号驱动IO)在实际中并不常用,所以主要介绍其余四种IO Model。
"""
同步异步
阻塞非阻塞
常见的网络阻塞状态:
accept
recv
recvfrom
send虽然它也有io行为 但是不在我们的考虑范围
IO操作的流程
1)等待数据准备 (Waiting for the data to be ready)
2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
1. 阻塞IO模型
遇到read操作时程序就会一直等待数据,也就是在等待数据准备阶段一直阻塞
直到接收到需要的数据才会拷贝数据到进程中,这个拷贝过程也是阻塞态,然后才解除阻塞态,继续执行后面代码
该模型的特点是wait data阶段和copy阶段都阻塞,导致程序的运行效率低
"""
我们之前写的都是阻塞IO模型 协程除外
"""
import socket
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn, addr = server.accept()
while True:
try:
data = conn.recv(1024)
if len(data) == 0:break
print(data)
conn.send(data.upper())
except ConnectionResetError as e:
break
conn.close()
# 在服务端开设多进程或者多线程 进程池线程池 其实还是没有解决IO问题
该等的地方还是得等 没有规避
只不过多个人等待的彼此互不干扰
2. 非阻塞IO模型
非阻塞IO模型简介
遇到read操作时可以立即得到一个结果,可能为未收到数据或收到数据,
未收到数据得到的结果就是error,报错,但是需要再次发起read操作,
询问是否有数据,直到收到数据才停止
但是在两次read操作的间隔时间内,该程序可以执行一些别的操作,不用保持等待(阻塞)状态
非阻塞IO模型优缺点
该模型可以将数据准备阶段变为非阻塞状态,但是在这个阶段需要不断询问内核数据是否准备就绪
优点: 在数据准备阶段可以执行一些其他操作
缺点: 1.不断询问内核会大幅提高CPU的占用率,但是询问操作占用的CPU资源并未执行有效操作
2.询问存在间隔会导致IO操作响应慢,不间隔询问会导致占用CPU过高,可能导致死机
"""
要自己实现一个非阻塞IO模型
"""
import socket
import time
server = socket.socket()
server.bind(('127.0.0.1', 8081))
server.listen(5)
server.setblocking(False)
# 将所有的网络阻塞变为非阻塞
r_list = []
del_list = []
while True:
try:
conn, addr = server.accept()
r_list.append(conn)
except BlockingIOError:
# time.sleep(0.1)
# print('列表的长度:',len(r_list))
# print('做其他事')
for conn in r_list:
try:
data = conn.recv(1024) # 没有消息 报错
if len(data) == 0: # 客户端断开链接
conn.close() # 关闭conn
# 将无用的conn从r_list删除
del_list.append(conn)
continue
conn.send(data.upper())
except BlockingIOError:
continue
except ConnectionResetError:
conn.close()
del_list.append(conn)
# 挥手无用的链接
for conn in del_list:
r_list.remove(conn)
del_list.clear()
# 客户端
import socket
client = socket.socket()
client.connect(('127.0.0.1',8081))
while True:
client.send(b'hello world')
data = client.recv(1024)
print(data)
非阻塞IO模型总结
"""
虽然非阻塞IO给你的感觉非常的牛逼
但是该模型会 长时间占用着CPU并且不干活 让CPU不停的空转
我们实际应用中也不会考虑使用非阻塞IO模型
任何的技术点都有它存在的意义
实际应用或者是思想借鉴
"""
3. IO多路复用模型(重点)
目前大部分软件都在使用的技术点
IO多路复用模型简介
IO多路复用模型会通过操作系统的监管机制:如select对所有read操作的数据准备进行监管,
当得到数据后通知进程进行拷贝
操作系统的监管机制会将所有监管对象放在一起,询问数据是否准备完毕
IO多路复用模型优缺点
优点: 单个进程能同时处理多个网络连接中的read操作
缺点: 1.进程在数据准备阶段以及拷贝阶段依旧是阻塞的,只不过是改为了select阻塞,而不是IO阻塞
2.该模型在网络连接数量小时效率都不如IO阻塞模型
3.该模型需要轮询所有对象,而在网络连接数量大时会导致响应慢
4.不同的操作系统提供的更高性能的监管机制不同,跨平台性不好
"""
当监管的对象只有一个的时候 其实IO多路复用连阻塞IO都比比不上!!!
但是IO多路复用可以一次性监管很多个对象
server = socket.socket()
conn,addr = server.accept()
监管机制是操作系统本身就有的 如果你想要用该监管机制(select)
需要你导入对应的select模块
"""
import socket
import select
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
server.setblocking(False)
read_list = [server]
while True:
r_list, w_list, x_list = select.select(read_list, [], [])
"""
帮你监管
一旦有人来了 立刻给你返回对应的监管对象
"""
# print(res) # ([<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080)>], [], [])
# print(server)
# print(r_list)
for i in r_list: #
"""针对不同的对象做不同的处理"""
if i is server:
conn, addr = i.accept()
# 也应该添加到监管的队列中
read_list.append(conn)
else:
res = i.recv(1024)
if len(res) == 0:
i.close()
# 将无效的监管对象 移除
read_list.remove(i)
continue
print(res)
i.send(b'heiheiheiheihei')
# 客户端
import socket
client = socket.socket()
client.connect(('127.0.0.1',8080))
while True:
client.send(b'hello world')
data = client.recv(1024)
print(data)
监管机制:
"""
select机制,监管对象有上限 windows linux都有
poll机制 ,监管机制没上限 只在linux有
上述select和poll机制其实都不是很完美 当监管的对象特别多的时候可能会出现 极其大的延时响应
epoll机制 只在linux有
它给每一个监管对象都绑定一个回调机制
一旦有响应 回调机制立刻发起提醒
针对不同的操作系统还需要考虑不同检测机制 书写代码太多繁琐
有一个人能够根据你跑的平台的不同自动帮你选择对应的监管机制
selectors模块
"""
4. 异步IO模型(理想模型)
异步IO模型简介
异步IO模型中内核会在收到应用的read操作后立刻返回,解除阻塞,
然后由内核进行数据准备阶段和拷贝阶段,当这两个阶段完成后通知应用中的进程read已完成
"""
异步IO模型是所有模型中效率最高的
相关的模块和框架
模块:asyncio模块
异步框架:sanic tronado twisted
速度快!!!
"""
import threading
import asyncio
@asyncio.coroutine
def hello():
print('hello world %s'%threading.current_thread())
yield from asyncio.sleep(1) # 换成真正的IO操作
print('hello world %s' % threading.current_thread())
loop = asyncio.get_event_loop()
tasks = [hello(),hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
5. 信号驱动IO模型
信号驱动IO在实际中并不常用,所以主要介绍其余四种IO Model
IO模型对比
到目前为止,已经将四个IO Model都介绍完了。现在回过头来回答最初的那几个问题:blocking和non-blocking的区别在哪,synchronous IO和asynchronous IO的区别在哪。
先回答最简单的这个:blocking vs non-blocking。前面的介绍中其实已经很明确的说明了这两者的区别。调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
再说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。Stevens给出的定义(其实是POSIX的定义)是这样子的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,四个IO模型可以分为两大类,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO这一类,而 asynchronous I/O后一类 。
有人可能会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
经过上面的介绍,会发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。