并发编程

9.1 进程

9.1.1 基本概念

  • 定义:运行中的程序叫做进程

  • 程序和进程之间的区别: 程序只是一个文件,进程则代表这个文件被CPU运行起来了

  • 进程是计算机中最小的资源分配单位,进程的pid(port id)并不与进程绑定,每次随机分配。

9.1.2 进程的调度

操作系统调度进程的算法

  • 短作业优先

    • 根据来的任务的任务量大小来划分谁先谁后执行。

  • 先来先服务

    • 哪个任务先来,哪个任务先执行,其他需要等待。

  • 时间片轮转

    • 来的每个任务都轮流执行固定N分钟。

  • 多级反馈算法

    • 主要工作机制,设有N个队列(Q1,Q2....QN),其中各个队列对于处理机的优先级是不一样的,也就是说位于各个队列中的作业(进程)的优先级也是不一样的。

    • 对于N个作业,先来先服务,再来的等待,此时最先来的任务如果进行到规定时间片还没有执行完毕,会被重新放回到下一个队列等待,如果在Q2中此任务还不能完成,再依次放到下一个。

    • 单个队列遵循 先来先服务和时间片轮转原则,队列之间遵循短作业优先,Q1一定比Q2时间片短,且Q1优先级最高,,依次类推。即即使第QN个有N个人物在等待,第1~N-1个作业中有任意一个还在运行,此N任务中作业就不执行,只有上述全部处理完毕,才执行QN。

9.1.3 并行和并发

  • 并行

    • 两个程序,两个cpu,每个程序分别占用一个cpu自己执行自己的进行

    • 看起来是同时执行,实际在每一个时间点都在各自执行执行着

  • 并发

    • 两个程序,一个cpu,每个程序交替的在一个cpu上执行

    • 看起来是同时执行,但是实际上仍然是串行

9.1.4 同步/异步 阻塞/非阻塞

  • 同步

    • 定义:当程序1调用程序2时,程序1停下不动,直到程序2完成回到程序1来,程序1才继续执行下去。

    • 同步阻塞:调用函数,这个函数并没有完全利用cpu,即存在io操作

    • 同步非阻塞:调用函数,整个运行过程中没有io操作

  • 异步

    • 定义:当程序1调用程序2时,程序1径自继续自己的下一个动作,不受程序2的的影响。

    • 异步非阻塞:

      1.func扔到其他异步任务里执行

      2.本身的任务和func任务各自执行各自的,没有io操作

    • 异步阻塞:\

  • 阻塞

    • 阻塞的概念往往伴随着线程。线程一般是指:在调用结果返回前,当前线程会被挂起。调用线程只有在得到结果后才会被唤醒执行后续的操作。

  • 非阻塞

    • 阻塞的反向操作,非阻塞的调用是指:在结果没有返回前,该调用不会阻塞住当前的线程

  • 阻塞/非阻塞 同步/异步

    • 两者有本质的区别,主要是面向对象的不同

    • 阻塞/非阻塞:进程/线程需要操作的数据如果尚未就绪,是否妨碍了当前进程/线程的后续操作。

    • 同步/异步 :数据尚未准备就绪,是否等待数据结果。

    • 同步异步指的是调用方式,阻塞和非阻塞指的是当前的执行状态

  • 运行的三状态图

    • 就绪、运行、阻塞 **

9.1.5 multi processing多元进程

#开启一个进程
#函数名
#from multiprocessing import Process
p = Process(target=函数名,args=(参数1,参数2))
p.start()
import time
from multiprocessing import Process
def func():
   print('start',os.getpid())
   time.sleep(1)
   print('end',os.getpid())
if __name__ == '__main__':
   p = Process(target = func) # 创建一个即将要执行eat函数的进程对象
   p.start()   # 开启一个进程 异步程序   即不等待进程开启,也不等待操作系统的响应,只负责通知操作系统进行,开启进程后,主进程和子进程 各自进行。
   print('main:',os.getpid())

9.1.6 子进程

  • 父进程 在父进程创建了子进程

  • 在pycharm中启动的所有py程序读是pycharm的子进程

  • 父进程会等待所有的子进程结束以后才结束,目的是为了回收资源,如果子进程执行结束,父进程没有回收资源,那么这个子进程会变成一个僵尸进程

9.1.7 主进程的结束逻辑

  1. 主进程的代码结束

  2. 所有的子进程结束

  3. 给子进程回收资源

  4. 主进程结束

9.1.8 进程开启 操作系统windows和linux/ios之间的区别

开启进程的过程需要放在if __name__ == '__main__'下

  • window中,需要相当于在子进程中把主进程文件又从头到尾执行了一遍,除了放在if__name__ == '__main__'下的代码

  • linux中 不执行代码,而是把当前代码全部加载到内存,直接执行调用内存中的func函数

9.1.9 join方法

  1. 把一个进程的结束时间封装成一个join方法

  2. 执行join方法的效果就是,阻塞直到子进程执行结束时就执行阻塞

l= []
for i in range(10):
p = Process(target=函数名,args=(参数1,参数 p.start()
l.append(p)#在多个子进程中使用join
print(l)
for p in l:
p.join()
#阻塞 直到上面的十个进程都结束
print('5000封邮件已发送完毕')

9.1.10 守护进程(daemon)

守护进程(daemon)

  • 定义:系统引导的时候启动,并且一直运行直到系统关闭

  • 守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children

  • daemon = True 设置为守护进程

  • 守护进程随着主进程的代码结束而结束

  • 应用场景:生产者消费模型~守护线程对比~

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)

9.1.11 terminate使用

  • 作用,向指定子进程发出信号关闭,但是不会立马关闭,也不影响下面的程序

if __name__ == '__main__':
   p = Process(target=son1)
   p.start()           #异步
   print(p.is_alive())
   time.sleep(1)
   p.terminate()       #典型的异步非阻塞   向子进程发出信号,关闭,但是不影响下面程序
   print(p.is_alive())  # 判断进程是否还在,进程还在
   time.sleep(0.01)     #
   print(p.is_alive())  #操作系统已经响应了我们要关闭进程的需求,再去检测的时候,进程结束了

9.1.12 开启进程的方式

  • 面向函数

def  #函数名: 要在子进程中执行的代码
p = Process(target=函数名,arg=(参数1,))
  • 面向对象

class 类名(Process):
   def __init__(self,参数1,参数2) #如果子进程没参数不用写 初始化方法
   self.a = 参数1
   self.b = 参数2
   super().__init()  #继承父类的init方法,父类的init中有设定相关参数的一些方法,不用super继承会阉割掉这些方法,从而导致调用这个类的相关方法的时候报错
   def run(self):
       #要在子进程中执行的代码
p = 类名(参数)
Process提供操作进程的方法
p.start() #开启进程       异步非阻塞
p.terminate()  #结束进程,但是也是发命令,滞后   异步非阻塞
p.join()   #同步阻塞 子进程结束 就结束
p.isalive()  #获取当前进程的状态 True/False

9.1.13 进程之间的通信

  1. 进程之间的通信 - IPC(inter process communication)

  2. 第三方基于网络的 redis rabbitmq kafaka memcache

  3. Queue基于 天生就是数据安全的 即文件家族的socket pickle lock

  4. pipe 管道(不安全的) = 文件家族的socket pickle

  5. 队列 = 管道 + 锁

  6. put_nowait() / 往队列里放 使队列不阻塞,但是会报错异常,使用import queue 导入异常 queue.full 解除异常 get_nowait() / 接收队列里的值不阻塞,但是也会报错异常,实用性比较高,用于不确定队列里是否还有值,是就接受,没有就不阻塞。

    import queue
    q = queue(5)
    try:
       q.putnowait(6)
    except queue.full:
       pass
  7. put() / get() 队列中放值/取值 但是都是阻塞的

 from multiprocessing import Queue,Process
# 先进先出
def func(exp,q):  
   ret = eval(exp)
q.put({ret,2,3})
q.put(ret*2)
q.put(ret*4)

if __name__ == '__main__':
q = Queue()
Process(target=func,args=('1+2+3',q)).start()
print(q.get())
print(q.get())
print(q.get())  
  1. q.empty() 判断队列为空 / q.qsize() 判断队列中的个数 /q.full() 判断队列是否为满 ##但是不准确,计算的同时也可能队列状态也有变化。

9.1.14 生产者消费者模型

生产者消费模型

  • 一个进程就是一个生产者

  • 一个进程就是一个消费者

  • 定义:

    • 把一个产生数据并且处理数据的过程解耦

    • 让生产的数据的过程和处理数据的过程达到一个工作效率上的平衡

    • 中间的容器,在多进程中我们使用队列或者可被join的队列,做到控制数据的量

    • 当数据过剩的时候,队列的大小会控制这生产者的行为

    • 当数据严重不足的时候,队列会控制消费者的行为

    • 并且我们还可以通过定期检查队列中元素的个数来调节生产者消费者的个数

  • 队列:生产者和消费者之间的容器就是队列

import time
import random
from multiprocessing import Process,Queue
#生产者,设定每个生产10
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))
#消费者,视生产数量随机消费 while循环
def consumer(q,name):
while True:
food = q.get()
if not food:break
time.sleep(random.randint(1,3))
print('%s吃了%s'%(name,food))
# c_count 消费者数量   #p_count 生产者数量
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):  #在每个进程中 添加None 识别标记   帮助消费者停止消费
       q.put(None)
if __name__ == '__main__':
   cp(3,1)
  • JoinableQueue 模型

    • 在生产者阻塞等待队列完成,给消费者增加task_done返回队列是否完成提示,并添加消费者进程为守护进程,队列结束,生产者结束,主进程结束,消费者结束

    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()            #直到每个队列中的每个数据 都被取走并task_done才结束

    def consumer(q,name):
       while True:
           food = q.get()
           time.sleep(random.random())
           print('%s吃了%s'%(name,food))
           q.task_done()    #只有这一步,队列中才会真正执行-1

    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()

9.1.15 进程之间的数据共享

  • mulprocessing中有一个manager类,封装了所有和进程相关的 数据共享 数据传递

  • 相关的数据类型,对于 字典 列表这一类的数据操作的时候会产生数据不安全。需要加锁解决问题,并且需要尽量少的使用这种方式

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)

9.2 线程

9.2.1 基本概念

  • 是进程的一部分

  • 每一个进程中至少有一个线程,线程是负责执行具体代码的

  • 线程是计算机中能被CPU调度的最小单位(进程负责圈资源,线程负责执行具体代码)

  • 一个进程的多个线程是可以共享这个进程的数据的,即数据共享

9.2.2 跟进程对比

线程的创建,也需要一些开销(一个存储局部变量的结构,记录状态)

  • 线程的创建、切换和销毁开销远远小于进程

  • 进程:数据隔离, 开销大,同时执行几段代码,cpython下可以利用多核

  • 线程:数据共享,开销小,同时执行几段代码,cpython下不能利用多核,只能单线程进行

9.2.3 cpython解释器下的线程问题

  • cpython解释器 不能实现多线程利用多核

  • 锁 全局GIL 全局解释器锁,定义:cpython解释器中的机制导致了在同一个进程中多个线程不能同时利用多核 —— python的多线程只能是并发不能是并行

  • 保证了整个python程序中,只能有一个线程被CPU执行

  • 原因:cpython解释器中特殊的垃圾回收机制(gc)。

    • 垃圾回收机制是一条线程,来完成垃圾的回收,对各个程序的变量统计应用计数,当计数为0,进行回收。但是在统计每个程序的过程中,可能存在多条线程对同一个变量进行操作,由于没锁,导致引用计数异常,比如引用计数最开始统计的2次,即A线程引用1次,B线程引用1次,A取消引用的时候,垃圾回收线程进行监听,但是垃圾回收线程无法同时监听多个线程(关键),导致监听A的时候 如果B也取消,无法监听,则最后引用次数是1,永远不能改变了,为了改变这一情况,GIL规定一次只能有一个线程对引用次数进行操作,即多个线程顺序执行。

  • GIL锁导致了线程不能并行,可以并发

  • 所以使用单线程并不影响高io型的操作

  • 只会对高计算型的程序,即线程上少有或者基本没有IO操作,有效率上的影响

  • 遇到高计算 : 多进程 + 多线程 (python的多进程可以使用多核)

9.2.4 thread类

  • multiprocessing 是完全仿照这threading的类写的

  • 主线程如果结束了,主进程也就结束了

  • terminate 在线程中不能从主线程结束一个子线程,子进程结束必须自己自主结束

  • 线程数据共享

  • 线程开启基本格式

def func():
   print('start son thread')
   time.sleep(1)
   print('end son thread',os.getpid())
# 启动线程 start
Thread(target=func).start()
print('start',os.getpid())
time.sleep(0.5)
print('end',os.getpid())
  • 开启多个子线程

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')
  • join 方法 阻塞 直到子线程执行结束,(跟线程一样)

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('子线程执行完毕')
  • 面向对象的方式启动线程

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)    #ident 线程id
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()得到的就是这个当前线程的信息,通过ident 可以获得当前线程id
print(enumerate())   #获得当前还存活的线程信息
print(active_count())   # =====len(enumerate())
  • 线程的守护线程

    • 有一个区别是守护线程一直等到所有的非守护线程都结束之后才结束(区别于进程)

    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)
    #守护线程一直等到所有的非守护线程都结束之后才结束
    #除了守护了主线程的代码之外也会守护子线程

9.2.5 线程安全的单例模式

import time
from threading import Lock
class Singleton(object):
   __instance = None
   lock = Lock()
   def __new__(cls, *args, **kwargs):
       if cls._instance:
           return cls._instance
       with cls.lock:
           if not cls.__instance:
               cls.__instance = super().__new__(cls)
       return cls.__instance
   def __init__(self,name,age):
       self.name = name
       self.age = age

def func():
   a = A('alex', 84)
   print(a)

from threading import Thread
for i in range(10):
   t = Thread(target=func)
   t.start()

9.3 协程

9.3.1 基本概念

  • 定义:能够在一个线程下的多个任务之间来回切换,那么每一个任务都是一个协程

  • 协程的优势和特点:

    • 在一条线程上最大限度的提高CPU的使用率,在一个任务中遇到IO的时候就切换到其他任务

    • 在cpython解释器下:由于多线程本身就不能利用多核,所以即便是开启了多个线程也只能轮流在一个CPU上执行。协程如果把所有的任务的IO操作都规避掉,只剩下需要使用CPU的操作。就意味着协程就可以做到提高CPU利用率的效果

    • 数据共享,数据安全

  • 多线程和协程

    • 线程切换需要操作系统,开销更大,操作系统不可控,给操作系统的压力大

      • 操作系统对IO操作的感知更加灵敏

    • 协程 切换需要python代码,开销更小,通过程序实现,用户操作可控,完全不会增加操作系统的压力、

      • 用户级别能够对IO操作的感知比较低

  • 协程的切换方式

    • 原生python yield asyncio

    • c语言完成的cpython greenlet gevent

9.3.2 协程的类

9.3.2.1 greenlet
  • 任务名 = greenlet(函数名) 协程开启一个函数

  • '要切换的任务名' + switch()

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()
9.3.2.2 gevent (基于greenlet切换)
  • 任务名= gevent.spawn(函数名) 不用再手动切换

  • gevent.joinall([任务1,任务2]) 一次性等待所有协程结束

  • 任务名.value 获得任务绑定的函数的返回值

import time
import gevent
from gevent import monkey
monkey.patch_all()    #导致以后 可以识别阻塞,比如time.sleep重命名为gevent.sleep
# 分辨gevent是否识别了我们写的代码中的io操作的方法
# 再patchall之前打印一下涉及到io操作的函数地址
# 再patchall之后打印一下涉及到io操作的函数地址
# 如果两个地址一致,说明gevent没有识别这个io,如果不一致说明识别了
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)
9.3.2.3 aysncio类
  • 只识别自己的协程方法,基于yield机制切换的

  • await 阻塞 协程函数这里要切换出去,还能保证一会儿再切回来

  • await 必须写在async函数里,async函数是协程函数

  • loop 事件循环

  • 所有的协程的执行 调度 都离不开这个loop

#启动一个任务
async def demo():     #aysnc 代表这是一个协程函数
   print('start')  
   await asynio.sleep(1)
   print('end')
loop = asyncio.get_event_loop()  # 创建一个事件循环
loop.run_until_complete(demo())  # 把demo任务丢到事件循环中去执行
#启动多个任务,并且没有返回值
async def demo():   # 协程方法
   print('start')
   await asyncio.sleep(1)  # 阻塞
   print('end')

loop = asyncio.get_event_loop()  # 创建一个事件循环
wait_obj = asyncio.wait([demo(),demo(),demo()])  #把demo任务三合一
loop.run_until_complete(wait_obj)
# 启动多个任务并且有返回值
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) #把demo任务丢到事件循环中去执行
for t in tasks:
   print(t.result())          #获取返回值

9.4 锁

9.4.1 进程中的锁

  • 如果在一个并发的场景下,涉及到某部分内容,是需要修改一些所有进程共享数据资源,就需要加锁来维护数据的安全

  • 在数据安全的基础上,才考虑效率问题

  • 加锁之后能够保证数据的安全性 但是也降低了程序的执行效率

  • 同步存在的意义:数据的安全性

  • 格式:

    • 在主进程中实例化lock = Lock(),可以通过参数传递把这把锁传递给子进程

    • 在子进程中 对需要加锁的代码 进行 with lock:with lock相当于lock.acquire()和lock.release()

  • 进程中需要加锁的场景

    • 共享的数据资源(文件、数据库)

    • 对资源进行修改、删除操作

def search_ticket(user):
   with open('ticket_count') as f:
       dic = json.load(f)
       print('%s查询结果 : %s张余票'%(user,dic['count']))

def buy_ticket(user,lock):
   with lock:
   # lock.acquire()   # 给这段代码加上一把锁
       time.sleep(0.02)
       with open('ticket_count') as f:
           dic = json.load(f)
       if dic['count'] > 0:
           print('%s买到票了'%(user))
           dic['count'] -= 1
       else:
           print('%s没买到票' % (user))
       time.sleep(0.02)
       with open('ticket_count','w') as f:
           json.dump(dic,f)
   # lock.release()   # 给这段代码解锁

def task(user, lock):
   search_ticket(user)
   with lock:
       buy_ticket(user, lock)

if __name__ == '__main__':
   lock = Lock()
   for i in range(10):
       p = Process(target=task,args=('user%s'%i,lock))
       p.start()

9.4.2 线程中的锁

  • 即便是线程,有GIL,也会出现数据不安全的问题

  • 加锁会影响程序的执行效率,但是保证了数据的安全

  • 在一些赋值类的操作,比如a += 1 (区别于append等用法)

a = 0
def func():
   global a
   a += 1

import dis
dis.dis(func)   #查看具体系统执行流程
  • 其线程具体的操作如下,同样对于lst[0] += 1 dic['key']-=1 也会有这个流程,但是假如在线程还未执行到储存这个操作,GIL锁切换到了下个线程(切换的可能性一种是当前线程遇到阻塞,另一种是执行了700行命令自动切换),在下一个线程进行了同样的操作,结果是两个线程都操作了 a+=1 结果 a 只加了一次。导致不安全

    0 LOAD_GLOBAL              0 (a)     #锁定a
    2 LOAD_CONST               1 (1)     #载入常数1
    4 INPLACE_ADD                        #进行‘+’这个操作  
    6 STORE_GLOBAL             0 (a)     #储存 #问题关键,未来得及储存就轮转
    8 LOAD_CONST               0 (None)  
    10 RETURN_VALUE
  • 这种情况区别于a.append(1),等数据方法,而区分的关键主要是看程序执行是否会执行拿出来载入再放回去这一步流程,如果没有拿出来载入这一步,就不会存在计数异常。

0 LOAD_GLOBAL              0 (a)       #锁定a
2 LOAD_ATTR                1 (append)  #方法 加载到内存
4 LOAD_CONST               1 (1)       #载入常量
6 CALL_FUNCTION            1           #调用方法   这里就结束了
8 POP_TOP                              #压栈
10 LOAD_CONST              0 (None)
12 RETURN_VALUE
  • 互斥锁

    • 在同一个线程当中,不能连续acquire多次

  • 使用互斥锁产生的一个死锁现象

    • 有多把锁交替使用的过程中,双方各release了一把锁,导致无法得到另一把锁,程序停滞无法进行

    • 死锁现象: 1 有多把锁 2 交替使用

      #典型死锁问题
      import time
      from threading import Lock,Thread
      noodle_lock = Lock()
      fork_lock = Lock()
      # noodle_lock = fork_lock = RLock()
      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()
  • 递归锁

    • 在同一个线程中,可以连续acuqire多次不会被锁住

    • 将多把互斥锁变成了一把递归锁

    • 递归锁也会发生死锁现象,多把锁交替使用的时候

    • 递归锁效率相对更低

      import time
      from threading import RLock,Thread
      noodle_lock = fork_lock = RLock()    #吃面和拿叉子合并成一把锁,这是关键,而不是递归锁解决,只是递归锁可以连续 acquire 和 release 这里去掉重复的acquire和release用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()
  • 互斥锁和递归锁的区别

    • 主要区别就是递归锁可以连续acuqire,而互斥锁连续acuqire会死锁,但是不代表递归锁不会死锁

    • 使用递归锁优化代码逻辑,尽量使用互斥锁替换,保证效率更高。

  • 队列、栈和优先级队列

    • queue 队列 先进先出, 写一个server,所有的请求放在队列里,先来先服务的思想

    • infoqueue (stack) 堆栈, 后进先出,算法(与递归相关)

    • PriorityQueue 优先级队列 自动排序,用户级别,告警级别

9.5 池

9.5.1 进程池

  • 预先的开启固定个数的进程数,当任务来的时候,直接提交给已经开好的进程,让这个进程去执行就可以了

  • 节省了进程,线程的开启、关闭、切换都需要时间

  • 并且减轻了操作系统调度的负担

  • 方法:submit(提交任务)、shutdown(阻塞等待任务完成)、有返回值,也可以接受参数

from concurrent.futures import ProcessPoolExecutor
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)  #类
   res = []
   for i in range(10):
       ret = p.submit(func,i,'xk')   #一共开启了五个进程,第五个结束 第六个开始
       res.append(ret)
for ret in res:
       print(ret.result())
   #p.shutdown()   # 关闭池之后就不能继续提交任务,并且会阻塞,直到已经提交的任务完成
   print('main',os.getpid())

9.5.2 线程池

  • 进程池可以开的任务个数是有限的,最多不可以超过cpu的个数的两倍,线程池开启的可以是进程池的很多倍。

  • 可以使用map/也可以使用submit,使用map不用result求返回值,但是map使用比较局限,只能传一个迭代对象。

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)
# map只能给线程的函数传递一个可迭代的对象,如range(10) 预设好的列表
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())

9.5.3 map方法

ret = tp.map(func,iterable) 
#迭代获取iterable中的内容,作为func的参数,让子线程来执行对应的任务,tp是线程池,但是如果是有参数的,无法传递
# 线程进程池都可以用
for i in ret: i 每一个都是任务的返回值

9.5.4 回调函数

  • 作用根据每个线程的执行速率,执行完毕后直接执行下一个函数,避免了统一结束,统一再执行下一个函数的情况。

  • 回调函数 无法接收返回值

  • 回调函数 要求传入的必须是一个对象,即不能是已经经过result()的结果,也就是不能使用map方法回调

ret.add_done_callback(函数名)
要在ret对应的任务执行完毕之后,直接继续执行add_done_callback绑定的函数中的内容,并且ret的结果会作为参数返回给绑定的函数

 

posted @ 2019-11-02 17:11  Kn19ht  阅读(187)  评论(0)    收藏  举报