并发编程目录
多道技术
允许多个应用程序同时进入到内存、并且 CPU 交替执行多个代码。
CPU 切换有两种情况:
- 当程序遇到 I/O 操作时,操作系统会剥夺该程序 CPU 的执行权力
- 当一个程序长时间占用 CPU 时,操作系统会剥夺该程序 CPU 的执行权力
进程调度
先来先服务调度算法短作业优先调度算法- 时间片轮转法+多级反馈队列
进程的三状态转换图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BZwlBq8c-1685849715215)(https://natsume-1316988601.cos.ap-chengdu.myqcloud.com/python_image/CPU%E5%88%87%E6%8D%A2.png)]
同步和异步
用来描述任务的提交方式
- 同步:任务提交之后原地等待任务的提交结果、期间不做如何事情
- 异步:任务提交之后不在原地等待任务的结果、而是总结做其他事
阻塞非阻塞
用来描述进程的运行状态
- 阻塞:阻塞态
- 非阻塞:就绪态运行态
同步阻塞、同步非阻塞、异步阻塞、异步非阻塞(速度最快)
创建进程
python 创建进程的模块:
'''
os.fork():windows系统支持
multiprocessing
subprocess
'''
multiprocessing
创建进程的第一种方式:
from multiprocessing import Process
import time
def func(name):
print(f"{name}进程开始")
time.sleep(5)
print(f"{name}进程结束")
if __name__ == '__main__':
p = Process(target=func, args=("小明",))
p.start()
print("主进程结束")
主进程结束
小明进程开始
小明进程结束
注意一定要加 main()判断!!!要不然代码会变成递归、报错。
创建进程的第二种方式:
class MyProcess(Process):
def __init__(self, name):
super(MyProcess, self).__init__()
self.task_name = name
def run(self) -> None:
print(f"{self.task_name}任务开始")
time.sleep(3)
print(f"{self.task_name}进程结束")
if __name__ == '__main__':
p = MyProcess('哈哈哈')
p.start()
print("主进程结束")
主进程结束
哈哈哈任务开始
哈哈哈进程结束
总结:创建进程就是在内存中申请一块内存空间,然后把需要运行的代码放进去,多个进程的内存空间,它们彼此是隔离的,进程与进程之间的数据,它们是没办法直接交互的如果想要交互,则可以借助第三方工具/模块
join 方法
p.star()
p.json()
注意进程的创建顺序都是随机的、都是由操作系统创建的!
进程间数据隔离
from multiprocessing import Process
a = 18
def func():
global a
a = 10
if __name__ == '__main__':
p = Process(target=func())
p.start()
p.join()
print(a)
10
所以两个进程间的数据是相互隔离的
进程号
Windows:tasklist (查看进程号:pid)
mac/linux:ps aux
代码演示:
from multiprocessing import Process, current_process
import os
def func():
print(f'进程号为:{current_process().pid}')
print(os.getpid())
time.sleep(10)
if __name__ == '__main__':
p = Process(target=func)
p.start()
print(f"主进程号为:{current_process().pid}")
print(os.getpid())
主进程号为:11712
11712
进程号为:18040
18040
所以查看进程号有两种方法:
- os.getpid:获取当前进程ID
- os.getppid:获取父进程ID
- current_process :获取当前进程 ID
p.terminate():杀死当前进程
p.is_alive():判断当前进程是否存在
-注意:由于代码不能直接杀死进程、只能通过向操作系统请求、所以有一定的延迟,要注意延迟。
僵尸进程、孤儿进程:
- 僵尸进程
'''
子进程死后,还会有一些资源占用(进程号,进程运行状态,运行时间等),等待父进程通过系统调用回收(收尸)
除了init进程之外,所有的进程,最后都会步入僵尸进程
危害:
子进程退出后,父进程没有及时处理,僵尸进程就会一直占用计算机资源
'''
- 孤儿进程
子进程还未结束、父进程就被回收了,之后子进程就会交由系统统一处理。
守护进程
守护进程:父程序死后、子程序也会死
p.daemon = True
p.star()
from multiprocessing import Process
import time
def task(name):
print(f'{name} 活着')
time.sleep(3)
print(f'{name} 死了')
if __name__ == '__main__':
p = Process(target=task, args=('妲己',))
p.daemon = True
p.start()
time.sleep(1)
print("纣王死了,妲己也要死")
print('全部结束')
妲己 活着
纣王死了,妲己也要死
全部结束
消息队列
队列:先进先出
管道+锁
堆栈:后进先出
进程间数据相互交互。
queue:
# 两种导入方法
from multiprocessing import Queue
Queue()
from multiprocessing import Queue
queue.Queue()
import queue
queue.Queue()
具体使用方法:
from multiprocessing import Queue
q = Queue(6)
q.put("a")
q.put(3)
v1 = q.get()
v2 = q.get()
print(v1, v2)
-1(存数据)当数据超过定义的最大数据量时、程序会阻塞。
q.put_nowait(value) -如果没位置了直接报错
q.put(value, timeout=3) -如果位置满了、且等待了3秒还是满的、报错
-2(取数据)当取得数据量超过了存的数据、阻塞
q.get_nowait(value) -队列没有数据、就报错
q.full() -判断数据是否已满、返回布尔值
q.empty() -判断队列是否为空
可能会不准确。
生产者消费者模式
生产者:产生或制造数据的
消费者:消费或者处理数据的
媒介:消费队列(数据交互的)
from multiprocessing import Queue, Process, JoinableQueue
import time
import random
def producer(name, food, q):
for i in range(8):
time.sleep(random.randint(2, 4))
q.put(food)
print(f"{name} 做了{food}{i}")
def consumer(name, q):
while True:
v = q.get()
time.sleep(random.randint(1,3))
print(f"{name} 吃了{v}")
q.task_done() # 告诉队列、我们已经拿走了一个数据、并且已经处理完了
'''
JoinableQueue
在Queue的基础上多了一个计数器机制,每put一个数据,计数器就加一
每调用一次task_done,计数器就减
当计数器为0的时候,就会走q.join后面的代码
'''
if __name__ == '__main__':
q = JoinableQueue()
p1 = Process(target=producer, args=("小当家", "黄金炒饭", q))
p2 = Process(target=producer, args=("神厨小福贵", "佛跳墙", q))
c1 = Process(target=consumer, args=("八戒", q))
c1.daemon = True # 设置守护进程、主进程死后、子进程也要死、而不是等待。
p1.start()
p2.start()
c1.start()
p1.join() # 必须加
p2.join() # 必须加(保证每个数据全部处理完)
q.join()
# 主进程死了、子进程也要陪葬。(设置守护进程)
互斥锁
当多个进程同时操作一个数据时、会发生数据错乱的情况、解决的方法就是给数据加锁
并发变串行,牺牲效率、提高安全。
import random
from multiprocessing import Process, Lock
import json
import time
def search_tickets(name):
with open('date/test03.json', 'r', encoding='utf-8') as f:
count = json.load(f)
print(f'{name}执行查询操作,余票为:{count["count"]}')
return count
def buy_tickets(name):
with open('date/test03.json', 'r', encoding='utf-8') as f:
count = json.load(f)
time.sleep(random.randint(1, 5))
if count.get('count') > 0:
count['count'] -= 1
with open('date/test03.json', 'w', encoding='utf-8') as f:
json.dump(count, f)
print(f'用户:{name},抢票成功')
else:
print(f'{name} 余票不足抢票失败')
def task(name, mutex):
search_tickets(name)
# 加锁
mutex.acquire()
buy_tickets(name)
# 释放锁
mutex.release()
if __name__ == '__main__':
mutex = Lock()
for i in range(1, 9):
p = Process(target=task, args=(' ', mutex))
p.start()
执行查询操作,余票为:4
执行查询操作,余票为:4
执行查询操作,余票为:4
执行查询操作,余票为:4
执行查询操作,余票为:4
执行查询操作,余票为:4
执行查询操作,余票为:4
执行查询操作,余票为:4
用户: ,抢票成功
用户: ,抢票成功
用户: ,抢票成功
用户: ,抢票成功
余票不足抢票失败
余票不足抢票失败
余票不足抢票失败
余票不足抢票失败
多线程、多进程对比:
在计算密集(多进程)和、IO 密集(多线程)对比:
os.cpu_count()-查看当前电脑CPU数量(几核)
from threading import Thread
from multiprocessing import Process
import time
def task():
res = 0
for i in range(100000000):
res += 1
if __name__ == '__main__':
l = []
ps = time.time()
for i in range(8):
# p = Process(target=task) # 16.27019715309143
p = Thread(target=task) # 31.66116213798523
p.start()
l.append(p)
for p in l:
p.join()
pe = time.time()
print(pe - ps)
总结:
- 多进程和多线程都有各自的优势、以后写代码可以多个进程下、写多个线程。
- 现在开发的程序、多数都是 IO 密集型
死锁
死锁现象基本是锁的乱用、加上并发造成的(抢锁和释放锁矛盾冲突)
import time
from threading import Thread, Lock, current_thread
from multiprocessing import Process
mutex1 = Lock()
mutex2 = Lock()
def test():
mutex1.acquire()
print(f"{current_thread().name} 抢到了锁1")
mutex2.acquire()
print(f"{current_thread().name} 抢到了锁2")
mutex2.release()
mutex1.release()
mutex2.acquire()
print(f"{current_thread().name} 抢到了锁1")
time.sleep(1) # 为了更直接展示
mutex1.acquire()
print(f"{current_thread().name} 抢到了锁2")
mutex1.release()
mutex2.release()
if __name__ == '__main__':
for i in range(10):
t = Thread(target=test)
t.start()
Thread-1 抢到了锁1
Thread-1 抢到了锁2
Thread-1 抢到了锁1
Thread-2 抢到了锁1
程序一直卡住了
递归锁 :RLock
内部有一个计数器、每 acquire 一次计数器就会+1,每 release 一次、计数器就会-1。只要计数器不为 0,其他人就不能抢这把锁。
可递归、不会阻塞。而 Lock 不能重复抢锁。
信号量
在不同的阶段、可能会对应不同的技术点,对于并发编程来说、它指的是“锁”。 它可以用来控制同时访问特定资源的线程数量,通常用于某些资源有明确访问数量的场景简单来说就是用于**限流** ```python from threading import Thread, Semaphore import time import random
sp = Semaphore(5)
def task(name):
sp.acquire()
print(name, “抢到了锁”)
time.sleep(random.randint(3,5))
sp.release()
if name == ‘main’:
for i in range(25):
t = Thread(target=task, args=(f"bike{i+1}号",))
t.start()
## Event 事件
> 子进程/线程等待子进程/线程(正常情况下)
```python
from threading import Thread, Event
import time
even = Event()
def bus():
print("公交车即将到站")
time.sleep(3)
print("公交车到站了")
even.set() # 发射了一个信号、车来了赶快上车
print()
def passenger(name):
print(name, "正在等车")
even.wait() # 等待公交车来
print(name, "上车了")
if __name__ == '__main__':
t = Thread(target=bus())
t.start()
for i in range(10):
pa = Thread(target=passenger, args=(f"person{i + 1}",))
pa.start()
池
池是用来保护计算机硬件安全的情况下、最大限制的利用计算机资源、降低了程序的运行效率、但保证了计算机硬件的安全。
主要应用:
- 线程池:ThreadPoolExcutor
- 进程池:ProcessPoolExcutor
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
使用:
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
pool = ThreadPoolExecutor(10) # 默认有值
def task(name):
print(name)
time.sleep(3)
for i in range(50):
pool.submit(task, i)
函数会按照要求创建线程
-线程池:线程是自动创建的、用于控制线程的数量。
-信号量(锁):线程自己创建、控制程序的执行、阻塞。
异步获取提交的数据:
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
pool = ThreadPoolExecutor() # 默认有值
def task(name):
print(name)
time.sleep(3)
return name +10
f_list = []
for i in range(50):
p = pool.submit(task, i)
f_list.append(p)
pool.shutdown() # 关闭线程池、等待线程池中所有的任务全都运行完毕 (类似于线程的join()方法)
for i in f_list:
print(i.result())
0
1
2
3
4
...
数据会先全部提交完毕后、代码才会继续往下走、然后执行获取返回结果的代码
对于进程池、进程池的进程是不会替换的、在不断的循环往复中、进程号都不会变、还是那几个进程。
异步回调机制
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
pool = ProcessPoolExecutor() # 默认有值
def task(name):
print(name)
time.sleep(3)
return name + 10
def call_back(res):
print("call_back", res.result())
if __name__ == '__main__':
f_list = []
for i in range(50):
pool.submit(task, i).add_done_callback(call_back)
主要区别:.add_done_callback(call_back) 增加了异步回调函数、使得当任务提交运行完后、立刻就能拿到结果。
0
1
2
3
4
5
6
7
8
call_back 10
9
10
call_back 11
call_back 12
协程
也可以称为微线程、它是一种用户态内的上下文切换技术:单线程下实现并发效果
进程:资源单位
线程:执行单位
协程:认为构造出来的:(切换+保存状态)
注意:切换不一定提示效率!!
#yield
[[yield]]
def f1():
n = 0
for i in range(100000000):
n +=1
yield
def f2():
g = f1()
n = 0
for i in range(100000000):
n +=1
next(g)
start = time.time()
f2()
end = time.time()
print(end - start)
# 17.651402950286865
gevent
:遇到 IO 就切换
pip install gevent
from gevent import monkey # 打补丁
monkey.patch_all() # 检测所有的IO操作
from gevent import spawn
import time
def da():
for i in range(3):
print('da')
time.sleep(2)
def ma():
for i in range(3):
print('damai')
time.sleep(2)
def buyao():
for i in range(3):
print('不要')
time.sleep(3)
s = time.time()
g1 = spawn(da)
g2 = spawn(ma)
g3 = spawn(buyao)
g1.join() # 加时间等待
g2.join() # 加等待
g3.join()
en = time.time()
print(en - s)
da
damai
不要
da
damai
不要
da
damai
不要
9.072951316833496
TCP 协程化
# server
from gevent import monkey
monkey.patch_all()
from gevent import spawn
import socket
def comm(conn):
while True:
try:
data = conn.recv(1024)
if not data:
break
conn.send(data.upper())
except:
break
conn.close()
def run(ip, port):
server = socket.socket() # 默认tcp协议
server.bind((ip, port))
server.listen(5)
while True:
conn, addr = server.accept()
spawn(comm,conn)
if __name__ == '__main__':
# run('127.0.0.1', 8080)
g = spawn(run, '127.0.0.1', 8002)
g.join()
# client
import socket
from threading import Thread, current_thread
def t_client():
client = socket.socket()
client.connect(('127.0.0.1', 8002))
n = 0
while True:
msg = f"{current_thread().name} say {n}"
client.send(msg.encode('utf-8'))
data = client.recv(1024)
print(data.decode('utf-8'))
n += 1
if __name__ == '__main__':
for _ in range(100):
Thread(target=t_client).start()
在服务端哪里、采用 spawn 改写、客户端用多线程模拟 1000 人同时访问,可承受
IO 模型
主要研究的是网络 IO
- 等待数据准备(waiting for the data to ready)
- 把数据从内核拷贝到进程(copy data)
网络 IO - accept
- recv
- send
阻塞 IO
非阻塞 IO
# server
import socket
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8990))
server.listen(5)
server.setblocking(False) # 将所有的阻塞变为非阻塞
c_list = []
d_list = []
while True:
try:
conn, addr = server.accept()
c_list.append(conn)
except BlockingIOError:
for conn in c_list:
try:
data = conn.recv(1024)
if not data:
conn.close()
d_list.append(conn)
conn.send(data.upper())
except BlockingIOError:
pass
except ConnectionResetError:
conn.close()
d_list.append(conn)
for conn in d_list:
c_list.remove(conn)
d_list.clear()
当然以上代码只是非阻塞 IO 的思路,不必较真。
- 太浪费计算机资源
IO 多路复用
import select
import socket
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8080))
server.listen(5)
server.setblocking(False)
input_server = [server]
while True:
rlist, wlist, xlist = select.select(input_server, [], [])
for i in rlist:
if i is server:
conn, addr = i.accept()
input_server.append(conn)
continue
try:
data = i.recv(1024)
if not data:
i.close()
input_server.remove(i)
continue
i.send(data.upper())
except ConnectionResetError:
i.close()
input_server.remove(i)
continue
监管机制:
- select (linux 和 Windows 都有)
- poll (只有 linux 有、且监管数量比 select 多的多)
缺点:由于监管过程类似与 for 循环的效果、所以在对象特别多的时候、延迟有点大
- epoll (linux)
它给每一个监管对象都绑定了一个回调机制,一旦有响应回调机制就会把可读对象放入就绪链表,epo‖只需要判断就绪链表是否为空,不需要每次都把所有的监管对象都遍历一遍,节省了 cpu 大量的时间,性能也得到了大幅度提升
selectors:自适应代码
因为每个管理机制都有各自的优势、所以为了实现不同服务端的高效并发性、大佬写了 selectors 库、自动根据终端选择合适的管理机制,做自动化代码。
# server
# coding : utf-8
# 夏目&青一
# @name:13-selectors服务端
# @time: 2023/5/24 23:10
import socket
import selectors
def accept(server):
conn, addr = server.accept()
sel.register(conn, selectors.EVENT_READ, read)
def read(conn):
try: # windows 版本兼容
data = conn.recv(1024)
if not data:
conn.close()
sel.unregister(conn)
return
conn.send(data.upper())
except ConnectionResetError:
conn.close()
sel.unregister(conn)
return
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8081))
server.listen(5)
server.setsockopt(False)
sel = selectors.DefaultSelector()
sel.register(server, selectors.EVENT_READ, accept) # 2设置可读监控 3回调函数
while True:
events = sel.select()
for key, mask in events:
callback = key.data
callback(key.fileobj)
异步 IO
实现库
- asyncio (python 3.4)
- tornado
- fastapi
- django 3
- sanic
- vibora
- quart
- twisted
- aiohttp
异步编程的基本流程
简化版:
await 后面只能跟一种对象:可等待对象(
协程对象、
task 对象、
future 对象
)
# 3.7之后
import asyncio
import time
from threading import current_thread
def recv():
print("开始")
asyncio.sleep(2)
print("结束")
async def f1():
print(f'任务:f1 开始 {current_thread().name}')
data = await recv()
print(data)
print(f'任务:f1 结束 {current_thread().name}')
async def f2():
print(f'任务:f1 开始 {current_thread().name}')
await recv()
print(f'任务:f1 结束 {current_thread().name}')
tasks = [f2(), f2()]
asyncio.run(asyncio.wait(tasks))
之前的写法介绍可参考:
async await
官网地址
async def cancel_me():
print('cancel_me(): before sleep')
try:
# Wait for 1 hour
await asyncio.sleep(3600)
except asyncio.CancelledError:
print('cancel_me(): cancel sleep')
raise
finally:
print('cancel_me(): after sleep')
async def main():
# Create a "cancel_me" Task
task = asyncio.create_task(cancel_me())
# Wait for 1 second
await asyncio.sleep(1)
task.cancel()
try:
await task
except asyncio.CancelledError:
print("main(): cancel_me is cancelled now")
asyncio.run(main())
异步迭代器:
迭代器:内置__iter__、next
# 普通的
class MyRange(object):
def __init__(self, start, end=None):
if end:
self.count = start - 1
self.end = end
else:
self.count = -1
self.end = start
def add_count(self):
self.count += 1
if self.count == self.end:
return None
return self.count
def __iter__(self):
return self
def __next__(self):
value = self.add_count()
if value is None:
raise StopIteration
return value
# 测试迭代器
for i in MyRange(10):
print(i)
# 输出1、2、3、4、、、
非协程改写
非协程函数需要用到协程的改写
def f1(x):
time.sleep(3)
return 'hellow'
async def main():
loop = asyncio.get_running_loop()
future = loop.run_in_executor(None,f1,'values')
res = await future
print(res)
asyncio.run(main())
把本不支持的函数、放到线程池里面、从而实现异步
异步上下文管理器
上下文管理器:
with open()
- 对象内部需要定义__enter__方法、__exit__方法
import socket
class Client(object):
def __init__(self, ip, port):
self.ip = ip
self.port = port
def __enter__(self):
self.c = socket.socket()
self.c.connect((self.ip,self.port))
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.c.close()
def recv(self):
pass
# 测试
with Client("127.0.0.1", 8080) as c:
c.recv()
就可以用支持 with 语法了
客户端协程代码(上下文形式)
class Client(object):
def __init__(self, ip, port):
self.ip = ip
self.port = port
self.loop = asyncio.get_running_loop()
async def __aenter__(self):
self.c = socket.socket()
# self.c.connect((self.ip,self.port))
# 异步链接服务器
await self.loop.sock_connect(self.c, (self.ip, self.port))
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
self.c.close()
async def recv(self):
data = await self.loop.sock_recv(self.c, 1024)
return data
async def send(self, data):
await self.loop.sock_sendall(self.c, data.encode('utf-8'))
# 测试
async def main():
async with Client('127.0.0.1', 8090) as c:
await c.send('abc')
data = await c.recv()
print(data)
asyncio.run(main())
服务端
import socket
import asyncio
async def waiter(conn, loop):
while True:
try:
data = await loop.sock_recv(conn, 1024)
if not data:
break
await loop.sock_sendall(conn, data.upper())
except ConnectionResetError:
break
conn.close()
async def main(ip, port):
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((ip, port))
server.listen(5)
server.setblocking(False)
loop = asyncio.get_running_loop()
while True:
conn, addr = await loop.sock_accept(server)
# 创建事件循环
loop.create_task(waiter(conn, loop))
asyncio.run(main('localhost',8090))
uvloop
通过改变 asyncio 时间循环默认的源、从而提高协程效率(两倍差不多)
import uvLoop
# 打补丁
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
学习笔记: