并发编程
并发编程
1.操作系统认识
-
操作系统种类
- 多道操作系统
- 在一个任务遇到IO时让出CPU
- 切换由操作系统完成,也需要耗时
- 特点
- 数据隔离
- 时空复用
- 在一个任务遇到IO时让出CPU
- 分时操作系统
- 给时间分片,让多个任务轮流使用CPU
- 时间分片
- CPU轮转
- 每一个程序分配一个时间片
- 切换要占用时间,反而降低了CPU的利用率,但是提高了用户体验
- 实时操作系统
- 网络操作系统
- 分布式操作系统
- 多道操作系统
-
目前常用的操作系统
- 分时操作系统 + 多道操作系统 + 实时操作系统
- 运行机制:多个程序一起在计算机中执行,一个程序如果遇到IO操作,切出去让出CPU,一个程序没有遇到IO,但是时间片到时了,切出去让出CPU
- 分时操作系统 + 多道操作系统 + 实时操作系统
-
操作系统调度进程的算法
- 短作业优先
- 先来先服务
- 时间片轮转
- 多级反馈算法
2.进程
2.1进程相关概念
-
进程的特点
- 开销大,数据隔离,最小资源分配单位,
cpython
下可以利用多核
- 开销大,数据隔离,最小资源分配单位,
-
进程的三状态
- 就绪
- 运行
- 阻塞
-
操作系统创建进程的方式不同
- windows操作系统执行开启进程的代码
- 实际上新的子进程需要通过import父进程的代码来完成数据的导入工作,所以有一些内容我们只希望在父进程中完成,就写在
if __name__ == '__main__':
下面
- 实际上新的子进程需要通过import父进程的代码来完成数据的导入工作,所以有一些内容我们只希望在父进程中完成,就写在
ios linux
操作系统创建进程 fork- 只copy代码而不执行,只执行要start的内容
- windows操作系统执行开启进程的代码
-
主进程和子进程之间的关系
- 主进程的代码结束——>所有的子进程结束——>给子进程回收资源——>主进程结束
2.2进程开启(multiprocession
)
from multiprocessing import Process
2.2.1面向函数开启
-
创建进程:使用Process类
-
基本模板
- 创建进程对象
p = Process(target=函数名, args=(参数1,))
- 开启进程
p.start()
- 给了操作系统一条指令,并不是立即开启,需要响应时间
- 创建进程对象
2.2.2面向对象开启进程
import os
import time
from multiprocessing import Process
class MyProcecss2(Process):
def run(self):
while True:
print('is alive')
time.sleep(0.5)
class MyProcecss1(Process):
def __init__(self,x,y):
self.x = x
self.y = y
super().__init__()
def run(self):
print(self.x,self.y,os.getpid())
for i in range(5):
print('in son2')
time.sleep(1)
if __name__ == '__main__':
mp = MyProcecss1(1,2)
mp.daemon = True
mp.start()
print(mp.is_alive())
mp.terminate()
# mp2 = MyProcecss2()
# mp2.start()
# print('main :',os.getpid())
# time.sleep(1)
- 注意点
- 一个类只能创建一个进程,如果想创建多个进程,那么构建多个类
- 要想传入参数,需要重写
__init__
方法,但重写的时候必须要保留原本的__init__
中的参数,使用super().__init__()
2.3守护进程
p.daemon = True
通过该方法将进程设置为守护进程- 特点
- 守护进程是随着主进程的代码结束而结束的
- 所有子进程都必须在主进程结束之前结束,由主进程来负责回收资源
import time
from multiprocessing import Process
def son1(a,b):
while True:
print('is alive')
time.sleep(0.5)
def son2():
for i in range(5):
print('in son2')
time.sleep(1)
if __name__ == '__main__':
p = Process(target=son1,args=(1,2))
p.daemon = True
p.start() # 把p子进程设置成了一个守护进程
p2 = Process(target=son2)
p2.start()
time.sleep(2)
2.4进程相关方法补充
-
os
模块中,获取进程id-
os.getpid()
def func(): print('start',os.getpid()) time.sleep(1) print('end',os.getpid())
-
-
Process类中
- join():阻塞直到子进程结束
import time import random from multiprocessing import Process def send_mail(a): time.sleep(random.random()) print('发送了一封邮件',a) if __name__ == '__main__': l = [] for i in range(10): p = Process(target=send_mail,args=(i,)) p.start() l.append(p) print(l) for p in l:p.join() # 阻塞 直到上面的十个进程都结束 print('5000封邮件已发送完毕')
p.is_alive()
判断进程是否还在运行p.terminate()
结束进程,异步非阻塞的p.start()
和p.terminate()
方法都是异步非阻塞的
2.5进程之间的数据共享
2.5.1Manage
from multiprocessing import Manager,Process,Lock
def func(dic,lock):
with lock:
dic['count'] -= 1
if __name__ == '__main__':
# m = Manager()
with Manager() as m:
l = Lock()
dic = m.dict({'count':100})
p_l = []
for i in range(100):
p = Process(target=func,args=(dic,l))
p.start()
p_l.append(p)
for p in p_l:p.join()
print(dic)
- 注意点:加锁
- 封装了所有和进程相关的数据共享、数据传递相关的数据类型,但是对于 字典 列表这一类的数据操作的时候会产生数据不安全,需要加锁解决问题,并且需要尽量少的使用这种方式
2.5.2IPC机制
- ipc机制 :队列 管道
- 第三方工具(软件)提供给我们的IPC机制
- redis
- memcache
- kafka
- rabbitmq
- 特点
- 并发需求
- 高可用
- 断电保存数据
- 解耦
3.线程
3.1线程相关概念
- 线程的特点
- 开销小,数据共享,
cpu
调度单位,cpython
下不能利用多核
- 开销小,数据共享,
3.2线程开启(threading)
from threading import Thread
3.2.1面向函数开启
import time
from threading import Thread
def func(i):
print('start son thread',i)
time.sleep(1)
print('end son thread',i,os.getpid())
for i in range(10):
Thread(target=func,args=(i,)).start()
print('main')
3.2.2面向对象开启
import time
from threading import Thread
class MyThread(Thread):
def __init__(self,i):
self.i = i
super().__init__()
def run(self):
print('start',self.i,self.ident)
time.sleep(1)
print('end',self.i)
for i in range(10):
t = MyThread(i)
t.start()
print(t.ident)
3.3守护线程
import time
from threading import Thread
def son1():
while True:
time.sleep(0.5)
print('in son1')
def son2():
for i in range(5):
time.sleep(1)
print('in son2')
t =Thread(target=son1)
t.daemon = True
t.start()
Thread(target=son2).start()
time.sleep(3)
- 注意点
- 守护线程一直等到所有的非守护线程都结束之后才结束,除了守护了主线程的代码之外也会守护子线程
3.4线程相关方法补充
-
join方法:阻塞直到子线程执行结束
import time from threading import Thread def func(i): print('start son thread',i) time.sleep(1) print('end son thread',i,os.getpid()) t_l = [] for i in range(10): t = Thread(target=func,args=(i,)) t.start() t_l.append(t) for t in t_l:t.join() print('子线程执行完毕')
-
current_thread类,表示当前线程
-
ident方法,线程id号
-
enumerate类,表示有几个线程开启
-
active_count类,开启线程的个数
from threading import current_thread,enumerate,active_count
def func(i):
t = current_thread()
print('start son thread',i,t.ident)
time.sleep(1)
print('end son thread',i,os.getpid())
t = Thread(target=func,args=(1,))
t.start()
print(t.ident)
print(current_thread().ident) # 水性杨花 在哪一个线程里,current_thread()得到的就是这个当前线程的信息
print(enumerate())
print(active_count()) # =====len(enumerate())
3.5GIL全局解释器锁
- cpython解释器不能实现多线程利用多核
- 锁:GIL全局解释器锁
- 保证了整个python程序中,只能有一个线程被CPU执行
- 原因:cpython解释器中特殊的垃圾回收机制
- GIL锁导致了线程不能并行,可以并发
- 所以使用多线程并不影响高io型的操作,只会对高计算型的程序由效率上的影响
- 遇到高计算
- 多进程 + 多线程
- 分布式
4.生产者消费者模型
4.1使用队列
-
from multiprocessing import Process,Queue
-
解耦:修改方便,复用度高,可读性好
- 把写在一起的大的功能分开成多个小的功能处理
-
进程
- 一个进程就是一个生产者
- 一个进程就是一个消费者
-
队列
- 生产者和消费者之间的容器就是队列
-
代码
import time import random from multiprocessing import Process,Queue def producer(q,name,food): for i in range(10): time.sleep(random.random()) fd = '%s%s'%(food,i) q.put(fd) print('%s生产了一个%s'%(name,food)) def consumer(q,name): while True: food = q.get() if not food:break time.sleep(random.randint(1,3)) print('%s吃了%s'%(name,food)) def cp(c_count,p_count): q = Queue(10) for i in range(c_count): Process(target=consumer, args=(q, 'alex')).start() p_l = [] for i in range(p_count): p1 = Process(target=producer, args=(q, 'wusir', '泔水')) p1.start() p_l.append(p1) for p in p_l:p.join() for i in range(c_count): q.put(None) if __name__ == '__main__': cp(2,3)
4.2JoinableQueue
import time
import random
from multiprocessing import JoinableQueue,Process
def producer(q,name,food):
for i in range(10):
time.sleep(random.random())
fd = '%s%s'%(food,i)
q.put(fd)
print('%s生产了一个%s'%(name,food))
q.join() #####
def consumer(q,name):
while True:
food = q.get()
time.sleep(random.random())
print('%s吃了%s'%(name,food))
q.task_done() #####
if __name__ == '__main__':
jq = JoinableQueue()
p =Process(target=producer,args=(jq,'wusir','泔水'))
p.start()
c = Process(target=consumer,args=(jq,'alex'))
c.daemon = True
c.start()
p.join()
5.协程
5.1定义
- 用户级别的,由我们自己写的python代码来控制切换的,是操作系统不可见的
5.2多线程和协程
- 在cpython解释器下,协程和线程都不能利用多核,都是在一个CPU上轮转执行
- 由于多线程本身就不能利用多核,所以即使开启了多个线程也只能轮流在一个CPU上执行
- 协程如果把所有的任务的IO操作都规避掉,就剩下只需要使用CPU的操作了,就意味着协程就可以做到提高CPU利用率的效果
- 多线程和协程
- 线程:切换需要操作系统,开销大,操作系统不可控,给操作系统的压力大
- 操作系统对IO操作的感知更加灵敏
- 协程:切换需要python代码,开销小,用户操作可控,完全不会增加操作系统的压力
- 用户级别对IO操作感知比较低
- 线程:切换需要操作系统,开销大,操作系统不可控,给操作系统的压力大
5.3协程的切换
- 两种切换方式
- 原生python完成,yield asyncio
- C语言完成的python模块 greenlet gevent
5.3.1C语言完成的python模块
5.3.1.1greenlet
模块
- switch方法
# greenlet
import time
from greenlet import greenlet
def eat():
print('wusir is eating')
time.sleep(0.5)
g2.switch()
print('wusir finished eat')
def sleep():
print('小马哥 is sleeping')
time.sleep(0.5)
print('小马哥 finished sleep')
g1.switch()
g1 = greenlet(eat)
g2 = greenlet(sleep)
g1.switch()
5.3.1.2.gevent模块
g1 = gevent.spawn(函数名)
创建一个协程任务,不会立刻执行,遇到阻塞才执行g1.join()
阻塞,直到协程任务g1完成为止from gevent import monkey
monkey.patch_all()
python中的阻塞,gevent不识别,因此需要执行该方法,将python的阻塞重写为gevent能够识别的阻塞
import time
print('-->',time.sleep)
import gevent
from gevent import monkey
monkey.patch_all()
def eat():
print('wusir is eating')
print('in eat: ',time.sleep)
time.sleep(1)
print('wusir finished eat')
def sleep():
print('小马哥 is sleeping')
time.sleep(1)
print('小马哥 finished sleep')
g1 = gevent.spawn(eat) # 创造一个协程任务
g2 = gevent.spawn(sleep) # 创造一个协程任务
g1.join() # 阻塞 直到g1任务完成为止
g2.join() # 阻塞 直到g1任务完成为止
-
gevent.joinall([g1,g2,g3])
将多个协程任务阻塞直到所有任务都结束import time import gevent from gevent import monkey monkey.patch_all() def eat(): print('wusir is eating') time.sleep(1) print('wusir finished eat') def sleep(): print('小马哥 is sleeping') time.sleep(1) print('小马哥 finished sleep') g_l = [] for i in range(10): g = gevent.spawn(eat) g_l.append(g) gevent.joinall(g_l)
-
g1.value
取到返回值,必须等到任务执行完才有返回值import time import gevent from gevent import monkey monkey.patch_all() def eat(): print('wusir is eating') time.sleep(1) print('wusir finished eat') return 'wusir***' def sleep(): print('小马哥 is sleeping') time.sleep(1) print('小马哥 finished sleep') return '小马哥666' g1 = gevent.spawn(eat) g2 = gevent.spawn(sleep) gevent.joinall([g1,g2]) print(g1.value) print(g2.value)
5.3.2.asyncio模块
5.3.2.1启动一个协程任务
-
注意点
- 必须要是协程方法
async def demo():
- 阻塞前必须有
await
- 必须创建事件循环
loop = asyncio.get_event_loop()
- 任务执行必须加括号
loop.run_until_complete(demo())
- 必须要是协程方法
-
loop = asyncio.get_event_loop()
-
loop.run_until_complete(demo())
import asyncio # 起一个任务 async def demo(): # 协程方法 print('start') await asyncio.sleep(1) # 阻塞 print('end') loop = asyncio.get_event_loop() # 创建一个事件循环 loop.run_until_complete(demo()) # 把demo任务丢到事件循环中去执行
5.3.2.2启动多个任务
-
启动多个任务,并且没有返回值
- 增加一个wait对象
wait_obj = asyncio.wait([demo(),demo(),demo()])
- 将wait对象丢到事件循环中去
loop.run_until_complete(wait_obj)
import asyncio # 启动多个任务,并且没有返回值 async def demo(): # 协程方法 print('start') await asyncio.sleep(1) # 阻塞 print('end') loop = asyncio.get_event_loop() # 创建一个事件循环 ## wait_obj = asyncio.wait([demo(),demo(),demo()]) ## loop.run_until_complete(wait_obj)
- 增加一个wait对象
-
启动多个任务并且有返回值
-
t1 = loop.create_task(demo())
创建任务对象 -
t.result()
获取对应任务的返回值async def demo(): # 协程方法 print('start') await asyncio.sleep(1) # 阻塞 print('end') return 123 loop = asyncio.get_event_loop() #### t1 = loop.create_task(demo()) t2 = loop.create_task(demo()) tasks = [t1,t2] wait_obj = asyncio.wait([t1,t2]) ### loop.run_until_complete(wait_obj) for t in tasks: print(t.result())
5.3.2.3谁先回来先取谁的结果
- 创建main协程方法
# 谁先回来先取谁的结果
import asyncio
async def demo(i): # 协程方法
print('start')
await asyncio.sleep(10-i) # 阻塞
print('end')
return i,123
async def main():
task_l = []
for i in range(10):
task = asyncio.ensure_future(demo(i))
task_l.append(task)
for ret in asyncio.as_completed(task_l):
res = await ret
print(res)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
6.基本概念
6.1并行和并发
- 并行
- 两个程序,两个CPU,每个程序分别占用一个CPU自己执行自己的
- 看起来是同时执行,实际上在每一个时间点上都在各自执行着
- 两个程序,两个CPU,每个程序分别占用一个CPU自己执行自己的
- 并发
- 两个程序,一个CPU,每一个程序交替的在一个CPU上执行
- 看起来在同时执行,但是实际上仍然是串行
- 两个程序,一个CPU,每一个程序交替的在一个CPU上执行
6.2同步和异步
- 同步
- 正在进行某个动作,要想执行另一个操作,需要停下当前这个动作,去执行另外一个操作,执行完后再继续这个动作
- 异步
- 进行某个动作的同时,可以执行另外一个操作
6.3阻塞和非阻塞
- 阻塞:CPU不工作
- 非阻塞:CPU工作
6.4同步阻塞和同步阻塞
-
同步阻塞
conn.recv()
socket
阻塞的TCP协议
-
同步非阻塞
- func() 没有io操作
- socket 非阻塞的tcp协议的时候
- 调用函数(这个函数内部不存在io操作)
6.5异步非阻塞和异步阻塞
- 异步非阻塞
- 把func扔到其他任务里去执行了
- 我本身的任务和func任务各自执行各自的 没有io操作
- 异步阻塞
7.锁
7.1互斥锁
-
在线程中,即便有GIL,也会出现数据不安全现象
- 操作的是全局变量
- 做以下操作
- += -= *= /+ 先计算再赋值才容易出现数据不安全的问题
- 包括
lst[0] += 1 dic['key']-=1
a = 0 def add_f(): global a for i in range(200000): a += 1 def sub_f(): global a for i in range(200000): a -= 1 from threading import Thread t1 = Thread(target=add_f) t1.start() t2 = Thread(target=sub_f) t2.start() t1.join() t2.join() print(a)
-
查看操作系统指令
dis.dis(函数名)
查看函数中的指令
a = 0 def func(): global a a -= 1 import dis dis.dis(func)
-
解决方法:使用互斥锁
- 加锁虽然影响了程序的执行效率,但是保证了数据的安全
a = 0 def add_f(lock): global a for i in range(200000): with lock: a += 1 def sub_f(lock): global a for i in range(200000): with lock: a -= 1 from threading import Thread,Lock lock = Lock() t1 = Thread(target=add_f,args=(lock,)) t1.start() t2 = Thread(target=sub_f,args=(lock,)) t2.start() t1.join() t2.join() print(a)
-
注意点:互斥锁是锁中的一种:在同一个线程中,不能连续acquire多次
from threading import Lock lock = Lock() lock.acquire() print('*'*20) lock.release() lock.acquire() print('-'*20) lock.release()
7.2死锁现象
-
死锁现象
- 有多把锁,一把以上
- 多把锁交替使用
-
解决方法:使用递归锁,将多把互斥锁变成了一把递归锁
- 递归锁:能快速解决问题,但执行效率差
- 递归锁也会发生死锁现象,多把锁交替使用的时候
- 递归锁也会发生死锁现象,多把锁交替使用的时候
- 优化代码逻辑
- 可以使用互斥锁,解决问题
- 执行效率相对好
- 解决问题的效率相对低
- 递归锁:能快速解决问题,但执行效率差
-
使用互斥锁解决死锁问题
import time from threading import Lock,Thread lock = Lock() def eat1(name,noodle_lock,fork_lock): lock.acquire() print('%s抢到面了'%name) print('%s抢到叉子了' % name) print('%s吃了一口面'%name) time.sleep(0.1) print('%s放下叉子了' % name) print('%s放下面了' % name) lock.release() def eat2(name,noodle_lock,fork_lock): lock.acquire() print('%s抢到叉子了' % name) print('%s抢到面了'%name) print('%s吃了一口面'%name) time.sleep(0.1) print('%s放下面了' % name) print('%s放下叉子了' % name) lock.release() lst = ['alex','wusir','taibai','yuan'] Thread(target=eat1,args=(lst[0],noodle_lock,fork_lock)).start() Thread(target=eat2,args=(lst[1],noodle_lock,fork_lock)).start() Thread(target=eat1,args=(lst[2],noodle_lock,fork_lock)).start() Thread(target=eat2,args=(lst[3],noodle_lock,fork_lock)).start()
7.3递归锁
-
递归锁:在同一个线程中,可以连续acuqire多次不会被锁住
import time from threading import Thread,Lock noodle_lock = Lock() fork_lock = Lock() def eat1(name,noodle_lock,fork_lock): noodle_lock.acquire() print('%s抢到面了'%name) fork_lock.acquire() print('%s抢到叉子了' % name) print('%s吃了一口面'%name) time.sleep(0.1) fork_lock.release() print('%s放下叉子了' % name) noodle_lock.release() print('%s放下面了' % name) def eat2(name,noodle_lock,fork_lock): fork_lock.acquire() print('%s抢到叉子了' % name) noodle_lock.acquire() print('%s抢到面了'%name) print('%s吃了一口面'%name) time.sleep(0.1) noodle_lock.release() print('%s放下面了' % name) fork_lock.release() print('%s放下叉子了' % name) lst = ['alex','wusir','taibai','yuan'] Thread(target=eat1,args=(lst[0],noodle_lock,fork_lock)).start() Thread(target=eat2,args=(lst[1],noodle_lock,fork_lock)).start() Thread(target=eat1,args=(lst[2],noodle_lock,fork_lock)).start() Thread(target=eat2,args=(lst[3],noodle_lock,fork_lock)).start()
8.队列
8.1先进先出队列
from queue import Queue
from queue import Queue # 先进先出队列
q = Queue(5)
q.put(0)
q.put(1)
q.put(2)
q.put(3)
q.put(4)
print('444444')
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())
8.2后进先出队列
-
from queue import LifoQueue
from queue import LifoQueue # 后进先出队列 # last in first out 栈 lfq = LifoQueue(4) lfq.put(1) lfq.put(3) lfq.put(2) print(lfq.get()) print(lfq.get()) print(lfq.get())
8.3优先级队列
-
from queue import PriorityQueue
from queue import PriorityQueue pq = PriorityQueue() pq.put((10,'alex')) pq.put((6,'wusir')) pq.put((20,'yuan')) print(pq.get()) print(pq.get()) print(pq.get())
9.池
9.1定义
-
使用
concurrent.futures
模块 -
预先的开启固定个数的进程数,当任务来临的时候,直接提交给已经开好的进程,让这个进程去执行就可以了,节省了进程,线程的开启 关闭 切换都需要时间,并且减轻了操作系统调度的负担
9.2进程池
- 开销大,一个池中的任务个数限制了我们程序的并发个数
- 不传参
import os
import time
import random
from concurrent.futures import ProcessPoolExecutor
# submit + shutdown
def func():
print('start',os.getpid())
time.sleep(random.randint(1,3))
print('end', os.getpid())
if __name__ == '__main__':
p = ProcessPoolExecutor(5)
for i in range(10):
p.submit(func)
p.shutdown() # 关闭池之后就不能继续提交任务,并且会阻塞,直到已经提交的任务完成
print('main',os.getpid())
-
传参
def func(i,name): print('start',os.getpid()) time.sleep(random.randint(1,3)) print('end', os.getpid()) return '%s * %s'%(i,os.getpid()) if __name__ == '__main__': p = ProcessPoolExecutor(5) ret_l = [] for i in range(10): ret = p.submit(func,i,'alex') ret_l.append(ret) for ret in ret_l: print('ret-->',ret.result()) # ret.result() 同步阻塞 print('main',os.getpid())
9.3线程池
-
不传参
import os import time import random from concurrent.futures import ThreadPoolExecutor def func(i): print('start', os.getpid()) time.sleep(random.randint(1,3)) print('end', os.getpid()) return '%s * %s'%(i,os.getpid()) tp = ThreadPoolExecutor(20) ret_l = [] for i in range(10): ret = tp.submit(func,i) ret_l.append(ret) tp.shutdown() print('main') for ret in ret_l: print('------>',ret.result())
-
传参
from concurrent.futures import ThreadPoolExecutor def func(i): print('start', os.getpid()) time.sleep(random.randint(1,3)) print('end', os.getpid()) return '%s * %s'%(i,os.getpid()) tp = ThreadPoolExecutor(20) ret = tp.map(func,range(20)) for i in ret: print(i) ret_l = [] for i in range(10): ret = tp.submit(func,i) ret_l.append(ret) tp.shutdown() print('main')
9.4回调函数
-
ret.add_done_callback(函数名)
-
要在ret对应的任务执行完毕之后,直接继续执行add_done_callback绑定的函数中的内容,并且ret的结果会作为参数返回给绑定的函数
import requests
from concurrent.futures import ThreadPoolExecutor
def get_page(url):
res = requests.get(url)
return {'url':url,'content':res.text}
def parserpage(ret):
dic = ret.result()
print(dic['url'])
tp = ThreadPoolExecutor(5)
url_lst = [
'http://www.baidu.com', # 3
'http://www.cnblogs.com', # 1
'http://www.douban.com', # 1
'http://www.tencent.com',
'http://www.cnblogs.com/Eva-J/articles/8306047.html',
'http://www.cnblogs.com/Eva-J/articles/7206498.html',
]
ret_l = []
for url in url_lst:
ret = tp.submit(get_page,url)
ret_l.append(ret)
ret.add_done_callback(parserpage)