python-并发编程

第十一章 并发编程

1 输入输出

  • I/O操作 相对内存来说

  • 输入(Input)

    • 输入是怎么输入 :键盘\input\read\recv

  • 输出(Output)

    • 输出是怎么输出 :显示器 打印机 播放音乐\print\write\send

文件操作 :read write
网络操作 :send recv recvfrom
函数     :print input
  • 计算机的工作分为两个状态【CPU的工作效率 500000000条指令/s】

    • CPU工作:做计算(对内存中的数据进行操作)的时候工作

    • CPU不工作:IO操作的时候

2 操作系统

1 多道操作系统

  • 一个程序遇到IO操作就把CPU让给别的程序,多个程序共同存在在一台计算机中,其中一个程序执行让出cpu之后,另一个程序能继续使用cpu,提高了CPU的利用率。单纯的切换还是会占用时间,但是多道操作系统的原理整体上还是节省了时间,提高了CPU的利用率。

  • 进程之间的数据是隔离

  • 时空复用:再同一个时间点上,多个程序同时执行着,一块儿内存条上存储了多个进程的数据

  • 什么是单处理机系统?

  • 一个计算机系统只包括一个运算处理器,则称之为单处理机系统。

    在单处理机计算机系统中,运行状态最多1个,最少0个;等待状态最多N个,最少N-1个;就绪状态最多N-1个,最少0个。

  • 单处理机系统中多道程序运行时的特点:

    (1)多道:计算机内存中同时存放几道相互独立的程序;

      (2)宏观上并行:同时进入系统的几道程序都处于运行过程中,即它们先后开始了各自的运行,但都未运行完毕;

      (3)微观上串行:实际上,各道程序轮流地用CPU,并交替运行。

  • 多道程序系统的出现,标志着操作系统渐趋成熟的阶段,先后出现了作业调度管理、处理机管理、存储器管理、外部设备管理、文件系统管理等功能。

    由于多个程序同时在计算机中运行,开始有了空间隔离的概念,只有内存空间的隔离,才能让数据更加安全、稳定。

    出了空间隔离之外,多道技术还第一次体现了时空复用的特点,遇到IO操作就切换程序,使得cpu的利用率提高了,计算机的工作效率也随之提高。

2 单CPU 分时操作系统

  • 把时间分成很小很小的段,每一个时间都是一个时间片,每一个程序轮流执行一个时间片的时间,自己的时间片到了就轮到下一个程序执行 -- 时间片的轮转

  • 分时系统的主要目标:对用户响应的及时性,即不至于用户等待每一个命令的处理时间过长。多用户分时系统是当今计算机操作系统中最普遍使用的一类操作系统。

  • 注意:分时系统的分时间片工作,在没有遇到IO操作的时候就用完了自己的时间片被切走了,这样的切换工作其实并没有提高cpu的效率,反而使得计算机的效率降低了。但是我们牺牲了一点效率,却实现了多个程序共同执行的效果,提高了用户体验,这样你就可以在计算机上一边听音乐一边聊qq了。

3 实时操作系统

  • 虽然多道批处理系统和分时系统能获得较令人满意的资源利用率和系统响应时间,但却不能满足实时控制与实时信息处理两个应用领域的需求。于是就产生了实时系统,即系统能够及时响应随机发生的外部事件,并在严格的时间范围内完成对该事件的处理。

    实时系统在一个特定的应用中常作为一种控制设备来使用。

       实时系统可分成两类:

       (1)实时控制系统。当用于飞机飞行、导弹发射等的自动控制时,要求计算机能尽快处理测量系统测得的数据,及时地对飞机或导弹进行控制,或将有关信息通过显示终端提供给决策人员。当用于轧钢、石化等工业生产过程控制时,也要求计算机能及时处理由各类传感器送来的数据,然后控制相应的执行机构。

       (2)实时信息处理系统。当用于预定飞机票、查询有关航班、航线、票价等事宜时,或当用于银行系统、情报检索系统时,都要求计算机能对终端设备发来的服务请求及时予以正确的回答。此类对响应及时性的要求稍弱于第一类。

      实时操作系统的主要特点

      (1)及时响应。每一个信息接收、分析处理和发送的过程必须在严格的时间限制内完成。

      (2)高可靠性。需采取冗余措施,双机系统前后台工作,也包括必要的保密措施等。

4 通用操作系统:

  • 具有多种类型操作特征的操作系统。可以同时兼有多道批处理、分时、实时处理的功能,或其中两种以上的功能。

5 个人计算机操作系统

个人计算机上的操作系统是联机交互的单用户操作系统,它提供的联机交互功能与通用分时系统提供的功能很相似。

  由于是个人专用,因此一些功能会简单得多。然而,由于个人计算机的应用普及,对于提供更方便友好的用户接口和丰富功能的文件系统的要求会愈来愈迫切。

6 网络操作系统

计算机网络:通过通信设施,将地理上分散的、具有自治功能的多个计算机系统互连起来,实现信息交换、资源共享、互操作和协作处理的系统。

  网络操作系统:在原来各自计算机操作系统上,按照网络体系结构的各个协议标准增加网络管理模块,其中包括:通信、资源共享、系统安全和各种网络应用服务。

7 分布式操作系统

表面上看,分布式系统与计算机网络系统没有多大区别。分布式操作系统也是通过通信网络,将地理上分散的具有自治功能的数据处理系统或计算机系统互连起来,实现信息交换和资源共享,协作完成任务。——硬件连接相同。

  但有如下一些明显的区别:

  (1)分布式系统要求一个统一的操作系统,实现系统操作的统一性。

  (2)分布式操作系统管理分布式系统中的所有资源,它负责全系统的资源分配和调度、任务划分、信息传输和控制协调工作,并为用户提供一个统一的界面。

  (3)用户通过这一界面,实现所需要的操作和使用系统资源,至于操作定在哪一台计算机上执行,或使用哪台计算机的资源,则是操作系统完成的,用户不必知道,此谓:系统的透明性。

  (4)分布式系统更强调分布式计算和处理,因此对于多机合作和系统重构、坚强性和容错能力有更高的要求,希望系统有:更短的响应时间、高吞吐量和高可靠性。

8 操作系统的作用

操作系统的主要功能是资源管理,程序控制和人机交互等。计算机系统的资源可分为设备资源和信息资源两大类。设备资源指的是组成计算机的硬件设备,如中央处理器,主存储器,磁盘存储器,打印机,磁带存储器, 显示器 ,键盘输入设备和鼠标等。信息资源指的是存放于计算机内的各种数据,如文件,程序库,知识库,系统软件和应用软件等。

  操作系统位于底层硬件与用户之间,是两者沟通的桥梁。用户可以通过操作系统的用户界面,输入命令。操作系统则对命令进行解释,驱动硬件设备,实现用户要求。以 现代 观点而言,一个标准个人电脑的OS应该提供以下的功能:进程管理(Processing management)内存管理(Memory management)文件系统(File system)网络通讯(Networking)安全机制(Security)用户界面驱动程序(Device drivers)

3 进程的概念

1 进程:一个运行中的程序就是一个进程

  • 占用资源 需要操作系统调度

  • pid : 在操作系统中用pid来够唯一标识一个进程

  • 计算机中最小的资源分配单位

2 进程的并发与并行

1并发:多个程序同时执行
  • 只有一个cpu,多个程序轮流在一个cpu上执行

  • 宏观上 : 多个程序在同时执行pid : 能够唯一标识一个进程

  • 微观上 : 多个程序轮流在一个cpu上执行 本质上还是串行

2 并行(好) :多个程序同时执行,并且同时在多个cpu上执行
3 区别

并行是从微观上,也就是在一个精确的时间片刻,有不同的程序在执行,这就要求必须有多个处理器。 并发是从宏观上,在一个时间段上可以看出是同时执行的,比如一个服务器同时处理多个session。

 

3 线程

  • 线程是进程中的一个单位,不能脱离进程存在

  • 线程是计算机中能够被CPU调度的最小单位:实际执行具体编译解释之后的代码的是线程,所以cpu执行的是解释之后的线程中的代码

4 进程的调度

4.1 进程的三状态图

 

4.2 进程的调度算法
  • 给所有的进程分配资源或者分配CPU使用权的一种方法

  • 短作业优先

  • 先来先服务

  • 多级反馈算法

    • 多个任务队列,优先级从高到低

    • 新来的任务总是优先级最高的

    • 每一个新任务几乎会立即获得一个时间片时间

    • 执行完一个时间片之后就会降到下一级队列中

    • 总是优先级高的任务都执行完才执行优先级低的队列

    • 并且优先级越高时间片越短

5 同步异步阻塞非阻塞

1.1 同步和异步
  • 所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。调用一个操作,要等待结果。

  • 所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。调用一个操作,不用等待结果。

1.2 阻塞与非阻塞
  • 阻塞和非阻塞这两个概念与程序(线程)等待消息通知(无所谓同步或者异步)时的状态有关。也就是说阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的

  • 阻塞:cup不工作:input accept recv recvfrom sleep connect

  • 非阻塞 :CPU在工作

1.3 同步/异步与阻塞/非阻塞
1 同步阻塞形式
  • 效率最低。拿排队的例子来说,就是你专心排队,什么别的事都不做。

  • 调用一个函数需要等待这个函数的执行结果,并且在执行这个函数的过程中CPU不工作

  • input,sleep,recv ,recvfrom, sleep ,accept ,connect ,get

2异步阻塞形式
  • 如果在银行等待办理业务的人采用的是异步的方式去等待消息被触发(通知),也就是领了一张小纸条,假如在这段时间里他不能离开银行做其它的事情,那么很显然,这个人被阻塞在了这个等待的操作上面:

    异步操作是可以被阻塞住的,只不过它不是在处理消息时阻塞,而是在等待消息通知时被阻塞。

  • 调用一个函数不需要等待这个函数的执行结果,需要等待,并且在执行这个函数的过程中CPU不工作

    • 开启10个进程 异步的

    • 获取这个进程的返回值,并且能做到哪一个进程先结束,就先获取谁的返回值

3 同步非阻塞形式
  • 实际上是效率低下的。

  想象一下你一边打着电话一边还需要抬头看到底队伍排到你了没有,如果把打电话和观察排队的位置看成是程序的两个操作的话,这个程序需要在这两种不同的行为之间来回的切换,效率可想而知是低下的。

  • 调用一个函数需要等待这个函数的执行结果,在执行这个函数的过程中CPU工作 调用了一个高计算的函数 strip eval('1+2+3') sum max min sorted

4 异步非阻塞形式
  • 效率更高,

  因为打电话是你(等待者)的事情,而通知你则是柜台(消息触发机制)的事情,程序没有在两种不同的操作中来回切换

  • 调用一个函数不需要等待这个函数的执行结果,不需要等待,并且在执行这个函数的过程中CPU工作 start() terminate()

6 进程的创建与结束
1 进程的创建

但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在。

  而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程:

  1. 系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)

  2. 一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)

  3. 用户的交互式请求,而创建一个新进程(如用户双击暴风影音)

  4. 一个批处理作业的初始化(只在大型机的批处理系统中应用)

  无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的。

2 进程的结束
  1. 正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在linux中用exit,在windows中用ExitProcess)

  2. 出错退出(自愿,python a.py中a.py不存在)

  3. 严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以捕捉异常,try...except...)

  4. 被其他进程杀死(非自愿,如kill -9)

7 在python程序中的进程操作
1 multiprocess模块
  • multiprocess不是一个模块而是python中一个操作、管理进程的包。 之所以叫multi是取自multiple的多功能的意思,在这个包中几乎包含了和进程有关的所有子模块。由于提供的子模块非常多,为了方便大家归类记忆,我将这部分大致分为四个部分:创建进程部分,进程同步部分,进程池部分,进程之间数据共享。

2 multiprocess.process模块
  • process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建。

  • 强调:
    1. 需要使用关键字的方式来指定参数
    2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号

    参数介绍:
    1 group参数未使用,值始终为None
    2 target表示调用对象,即子进程要执行的任务
    3 args表示调用对象的位置参数元组,args=(1,2,'egon',)
    4 kwargs表示调用对象的字典,kwargs={'name':'egon','age':18}
    5 name为子进程的名称
方法介绍
1 p.start():启动进程,并调用该子进程中的p.run() 
2 p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法  
3 p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
4 p.is_alive():如果p仍然运行,返回True
5 p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程
属性介绍
1 p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
2 p.name:进程的名称
3 p.pid:进程的pid
4 p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)
5 p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)
在windows中使用process模块的注意事项
在Windows操作系统中由于没有fork(linux操作系统中创建进程的机制),在创建子进程的时候会自动 import 启动它的这个文件,而在 import 的时候又执行了整个文件。因此如果将process()直接写在文件中就会无限递归创建子进程报错。所以必须把创建子进程的部分使用if __name__ ==‘__main__’ 判断保护起来,import 的时候  ,就不会递归运行了。
3 使用process模块创建进程
  • 在一个python进程中开启子进程,start方法和并发效果。

import time
from multiprocessing import Process

def f(name):
   print('hello', name) # pid process id           进程id
  # ppid parent process id   父进程id
   print('我是子进程')

if __name__ == '__main__':   # 只会在主进程中执行的所有的代码你写name=main下
   p = Process(target=f, args=('bob',))
   p.start()
   time.sleep(1)
   print('执行主进程的内容了')
   
   
   
   
import os
from multiprocessing import Process

def func(name,age):
   print(os.getpid(),os.getppid(),name,age)

if __name__ == '__main__':
   print('main :',os.getpid(),os.getppid())
   p = Process(target=func,args=('alex',84))  # 可以给子进程传递参数
   p.start()   # 主进程不能获取子进程的返回值
可以同时开启多个子进程 【多进程之间的数据是隔离的】
import os
import time
from multiprocessing import Process

def func(name,age):
   print('%s start'%name)
   time.sleep(1)
   print(os.getpid(),os.getppid(),name,age)

if __name__ == '__main__':
   print('main :',os.getpid(),os.getppid())
   arg_lst = [('alex',84),('太白', 40),('wusir', 48)]
   for arg in arg_lst:
       p = Process(target=func,args=arg)
       p.start()  # 异步非阻塞
4 join的用法
import os
import time
import random
from multiprocessing import Process

def func(name,age):
   print('发送一封邮件给%s岁的%s'%(age,name))
   time.sleep(random.random())
   print('发送完毕')

if __name__ == '__main__':
   arg_lst = [('大壮',40),('alex', 84), ('太白', 40), ('wusir', 48)]
   p_lst = []
   for arg in arg_lst:
       p = Process(target=func,args=arg)
       p.start()
       p_lst.append(p)
   for p in p_lst:p.join()   # join同步阻塞
   print('所有的邮件已发送完毕')
# join n 个进程   n个进程必须都执行完才继续
5 使用多进程实现一个并发的sokcet的server
import socket
from multiprocessing import Process


def talk(conn):
   while True:
       msg = conn.recv(1024).decode('utf-8')
       ret = msg.upper().encode('utf-8')
       conn.send(ret)
   conn.close()


if __name__ == '__main__':
   sk = socket.socket()
   sk.bind(('127.0.0.1', 9001))
   sk.listen()
   while True:
       conn, addr = sk.accept()
       Process(target=talk, args=(conn,)).start()

   sk.close()
import time
import socket

sk = socket.socket()
sk.connect(('127.0.0.1',9001))

while True:
   sk.send(b'hello')
   msg = sk.recv(1024).decode('utf-8')
   print(msg)
   time.sleep(0.5)

sk.close()
6 开启进程的另一种方法
  • 面向对象的方法,通过继承和重写run方法完成了启动子进程

  • 通过重写init和调用父类的init完成了给子进程传参数

import os
import time
from multiprocessing import Process


class MyProcess(Process):
   def __init__(self,a,b,c):
       self.a = a
       self.b = b
       self.c = c
       super().__init__()  # 调用基类的init

   def run(self):
       time.sleep(1)
       print(os.getpid(),os.getppid(), self.a, self.b, self.c)


if __name__ == '__main__':
   print('>>>',os.getpid())
   for i in range(10):
       p = MyProcess(1, 2, 3)  # 传参的方式
       p.start()
  • process类的其他方法

  • pid和ident一样是查看子进程的pid

  • name是查看子进程的名字

  • terminate 强制结束一个子进程

  • is_alive 查看子进程是否还活着

import os
import time
from multiprocessing import Process


class MyProcess(Process):

   def __init__(self, a, b, c):
       self.a = a
       self.b = b
       self.c = c
       super().__init__()

   def run(self):
       time.sleep(1)
       print(os.getpid(),os.getppid(),self.a,self.b,self.c)


if __name__ == '__main__':
   print('>>>',os.getpid())
   p = MyProcess(1,2,3)
   p.start()
   print(p.pid,p.ident)  # 查看子进程的pid
   print(p.name)  # 查看子进程的名字
   p.terminate()  # 强制结束一个子进程 是一个典型的异步非阻塞
   print(p.is_alive())  # 查看子进程是否还活着
   time.sleep(0.01)
   print(p.is_alive())
7 守护进程
# 主进程会等待所有的子进程结束,是为了回收子进程的资源
# 守护进程会等待主进程的代码执行结束之后再结束,而不是等待整个主进程结束.
# 主进程的代码什么时候结束,守护进程就什么时候结束,和其他子进程的执行进度无关
import time
from multiprocessing import Process


def son1():
   while True:
       print('-->','in son1')
       time.sleep(1)


def son2():
   for i in range(10):
       print('in son2')
       time.sleep(1)


if __name__ == '__main__':
   p1 = Process(target=son1)
   p1.daemon = True  # 表示设置p1为一个守护进程
   p1.start()
   p2 = Process(target=son2,)
   p2.start()
   time.sleep(4)
   print('in main')
# 要求守护进程p1必须在p2进程执行结束之后才结束
import time
from multiprocessing import Process

def son1():
   while True:
       print('--> in son1')
       time.sleep(1)

def son2():   # 执行10s
   for i in range(10):
       print('in son2')
       time.sleep(1)

if __name__ == '__main__':    # 3s
   p1 = Process(target=son1)
   p1.daemon = True    # 表示设置p1是一个守护进程
   p1.start()
   p2 = Process(target=son2,)
   p2.start()
   time.sleep(3)
   print('in main')
   p2.join()    # 等待p2结束之后才结束

# 等待p2结束 --> 主进程的代码才结束 --> 守护进程结束
# 一般情况下,多个进程的执行顺序,可能是:
   # 主进程代码结束--> 守护进程结束-->子进程结束-->主进程结束
   # 子进程结束 -->主进程代码结束-->守护进程结束-->主进程结束
8 锁【Lock】
# 锁:会降低程序的运行效率,保证数据的安全.
from multiprocessing import Lock # 互斥锁 不能再同一个进程中连续acquire多次
lock = Lock()
lock.acquire()
print(1)
lock.release()
lock.acquire()
print(2)
lock.release()
import time
from multiprocessing import Lock,Process
def func(i,lock):
   lock.acquire()   # 拿钥匙
   print('被锁起来的代码%s'%i)
   lock.release()  # 还钥匙
   time.sleep(1)

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

抢票的例子

import json
import time
from multiprocessing import Process,Lock

def search(i):
   with open('ticket',encoding='utf-8') as f:  # 读文件
       ticket = json.load(f)
   print('%s :当前的余票是%s张'%(i,ticket['count']))

def buy_ticket(i):
   with open('ticket',encoding='utf-8') as f:  # 读文件
       ticket = json.load(f)
   if ticket['count'] > 0:
       ticket['count'] -= 1
       print('%s买到票了' % i)
   time.sleep(0.1)
   with open('ticket', mode='w',encoding='utf-8') as f:
       json.dump(ticket,f)

def get_ticket(i,lock):
   search(i)
   with lock:   # 代替acquire和release 并且在此基础上做一些异常处理,保证即便一个进程的代码出错退出了,也会归还钥匙
       buy_ticket(i)


if __name__ == '__main__':
   lock = Lock()     # 互斥锁
   for i in range(10):
       Process(target=get_ticket,args=(i,lock)).start()
9 队列:生产者和消费者模型
# 进程之间如何实现通信
# 进城之间数据隔离(数据是安全的)
# 进程之间通信(IPC) Inter Process communication
   # 基于文件 :同一台机器上的多个进程之间通信
       # Queue 队列
           # 基于socket的文件级别的通信来完成数据传递的(基于socket\pickle\Lock实现的)
           # # pipe管道基于socket\pickle实现的,没有锁数据不安全
   # 基于网络 :同一台机器或者多台机器上的多进程间通信
       # 第三方工具(消息中间件)
           # memcache
           # redis
           # rabbitmq
           # kafka
from multiprocessing import Process,Queue

def pro(q):
   for i in range(10):
       print(q.get())

def son(q):
   for i in range(10):
       q.put('hello%s'% i)

if __name__ == '__main__':
   q = Queue()
   p = Process(target=son,args=(q,))
   p.start()
   p = Process(target=pro,args=(q,))
   p.start()

生产者和消费者模型

# 爬虫的时候
# 分布式操作 : celery
# 本质 :就是让生产数据和消费数据的效率达到平衡并且最大化的效率

# 把原本获取数据处理数据的完整过程进行了 解耦
# 把生产数据和消费数据分开,根据生产和消费的效率不同,来规划生产者和消费者的个数,让程序的执行效率达到平衡



# 解耦:拆分的很清楚的程序,松耦合的程序
# 如果你写了一个程序所有的功能\代码都放在一起,不分函数不分类也不分文件,紧耦合的程序
import requests
from multiprocessing import Process,Queue
url_dic = {
   'cnblogs':'https://www.cnblogs.com/Eva-J/articles/8253549.html',
   'douban':'https://www.douban.com/doulist/1596699/',
   'baidu':'https://www.baidu.com',
   'gitee':'https://gitee.com/old_boy_python_stack__22/teaching_plan/issues/IXSRZ',
}

def producer(name,url,q):
   ret = requests.get(url)
   q.put((name,ret.text))

def consumer(q):
   while True:
       tup = q.get()
       if tup is None:break
       with open('%s.html'%tup[0],encoding='utf-8',mode='w') as f:
           f.write(tup[1])

if __name__ == '__main__':
   q = Queue()
   pl = []
   for key in url_dic:
       p = Process(target=producer,args=(key,url_dic[key],q))
       p.start()
       pl.append(p)
   Process(target=consumer,args=(q,)).start()
   for p in pl:p.join()
   q.put(None)
import time
import random
from multiprocessing import Queue, Process


def consumer(q):  # 消费者:通常取到数据之后还要进行某些操作
   for i in range(10):
       print(q.get())


def producer(q):  # 生产者:通常在放数据之前需要先通过某些代码来获取数据
   for i in range(10):
       time.sleep(random.random())
       q.put(i)


if __name__ == '__main__':
   q = Queue()
   c1 = Process(target=consumer, args=(q,))
   p1 = Process(target=producer, args=(q,))
   c1.start()
   p1.start()
import time
import random
from multiprocessing import Process, Queue


def consumer(q, name):
   while True:
       food = q.get()
       if food:
           print('%s吃了%s' % (name, food))
       else:
           break


def producer(q,name,food):
   for i in range(10):
       foodi = '%s%s' % (food, i)
       print('%s生产了%s'%(name,foodi))
       time.sleep(random.random())
       q.put(foodi)


if __name__ == '__main__':
   q = Queue()
   p1 = Process(target=consumer,args=(q,'alex'))
   p2 = Process(target=producer,args=(q,'大壮','香蕉'))
   p1.start()
   p2.start()
   p2.join()
   q.put(None)
10 数据共享,Manager类
from multiprocessing import Process, Manager, Lock


def change_dic(dic, lock):
   with lock:
       dic['count'] -= 1


if __name__ == '__main__':
   m = Manager()
   lock = Lock()
   dic = m.dict({'count': 100})
   p_l = []
   for i in range(100):
       p = Process(target=change_dic, args=(dic, lock))
       p.start()
       p_l.append(p)
   for p in p_l: p.join()
   print(dic)
   
# Manager dict list 只要是共享的数据都存在数据不安全的现象,需要我们自己加锁来解决数据安全问题
总结
# 进程 : 数据隔离,资源分配的最小单位,可以利用多核,操作系统调度,数据不安全,开启关闭切换时间开销大
   # multiprocessing 如何开启进程 start join
   # 进程有数据不安全的问题 Lock (抢票的例子)
   # 进程之间可以通信ipc:
       # 队列(安全) 管道(不安全)
           # 生产者消费者模型
       # 第三方工具
   # 进程之间可以通过Manager类实现数据共享(不需要会写代码)
   # 一般情况下我们开启的进程数不会超过cpu个数的两倍

4 线程

  • 什么是线程?

  • 能被操作系统调度(给CPU执行)的最小单位

  • 在python中的特点:同一个进程中的多个线程可以同时被CPU执行,数据共享,操作系统调度的最小单位,可以利用多核,操作系统调度,数据不安全,开启关闭切换时间开销小

# 在CPython中的多线程 - 节省io操作的时间
   # gc 垃圾回收机制 线程
       # 引用计数 +分代回收
   # 全局解释器锁的出现主要是为了完成gc的回收机制,对不同线程的引用计数的变化记录的更加精准
   # 全局解释器锁 GIL(global interpreter lock)
       # 导致了同一个进程中的多个线程只能有一个线程真正被cpu执行
   # 节省的是io操作的时间,而不是cpu计算的时间,因为cpu的计算速度非常快,大部分情况下,我们没有办法把一条进程中所有的io操作都规避掉
   
   
# 在cpython解释器下 :GIL锁(全局解释器锁) 导致了同一个进程中的多个线程不能利用多核

# 正常的开发语言 多线程可以利用多核
# cpython解释器下的多个线程不能利用多核 : 就是规避了所有io操作的单线程
1 线程的开启
import os
import time
from threading import Thread, current_thread, enumerate, active_count


# from multiprocessing import Process as Thread   # 开进程
def func(i):
   print('start%s' % i, current_thread().ident)
   time.sleep(1)
   print('end%s' % i)


if __name__ == '__main__':
   tl = []
   for i in range(1):
       t = Thread(target=func, args=(i,))
       t.start()
       print(t.ident, os.getpid())
       tl.append(t)
   print(enumerate(), active_count())
   for t in tl: t.join()
   print('所有的线程都执行完了')

# current_thread() 获取当前所在的线程的对象 current_thread().ident通过ident可以获取线程id
# 线程是不能从外部terminate
# 所有的子线程只能是自己执行完代码之后就关闭
# enumerate 列表 存储了所有活着的线程对象,包括主线程
# active_count 数字 存储了所有活着的线程个数



enumerate 导入之后会和内置函数enumerate重名,需要做特殊的处理
from threading import enumerate as en
import threading
threading.enumerate()
2 面向对象的方式起线程
from threading import Thread
class MyThread(Thread):
   def __init__(self,a,b):
       self.a = a
       self.b = b
       super().__init__()

   def run(self):
       print(self.ident)

t = MyThread(1,2)
t.start()  # 开启线程 才在线程中执行run方法
print(t.ident)
3 数据共享
线程之间的数据的共享
from threading import Thread
n = 100

def func():
   global n
   n -= 1

t_l = []
for i in range(100):
   t = Thread(target=func)
   t.start()
   t_l.append(t)
for t in t_l:
   t.join()
print(n)
4 守护线程
import time
from threading import Thread


def son1():
   while True:
       print('in son')
       time.sleep(1)


def son2():
   for i in range(3):
       print('in son2')
       time.sleep(1)


t = Thread(target=son1)
t.daemon = True
t.start()
Thread(target=son2).start()

# 主线程会等待子线程结束之后才结束
# 为什么? # 主线程结束进程就会结束
# 守护线程随着主线程的结束而结束
# 守护线程会在主线程的代码结束之后继续守护其他子线程
# 守护线程会等待主线程(包括其他子线程)结束之后才结束
# 守护线程的结束原理,主进程结束了,守护线程和其他所有线程资源一起被回收掉了



# 守护进程 会随着主进程的代码结束而结束
   # 如果主进程代码结束之后还有其他子进程在运行,守护进程不守护
# 守护线程 随着主线程的结束而结束
   # 如果主线程代码结束之后还有其他子线程在运行,守护线程也守护
   
   
   
# 为什么?
   # 守护进程和守护线程的结束原理不同
   # 守护进程需要主进程来回收资源
   # 守护线程是随着进程的结束才结束的
       # 其他子线程-->主线程结束-->主进程结束-->整个进程中所有的资源都被回收-->守护线程也会被回收
       
       
# 进程是资源分配单位
# 子进程都需要它的父进程来回收资源
# 线程是进程中的资源
# 所有的线程都会随着进程的结束而被回收的
5 线程锁【重要】
1 线程数据不安全现象
from threading import Thread
import time
n = []
def append():
   for i in range(500000):
       n.append(1)
def pop():
   for i in range(500000):
       if not n:
           time.sleep(0.0000001)
       n.pop()


t_l = []
for i in range(20):
   t1 = Thread(target=append)
   t1.start()
   t2 = Thread(target=pop)
   t2.start()
   t_l.append(t1)
   t_l.append(t2)
for t in t_l:
   t.join()
print(n) # 会报错


# += -= *= /= while if 数据不安全   + 和 赋值是分开的两个操作
# append pop strip数据安全 列表中的方法或者字典中的方法去操作全局变量的时候 数据安全的
# 线程之间也存在数据不安全

# 判断数据是否安全
       # 是否数据共享,是同步还是异步 (数据共享并且异步)
       # += -= *= /= 赋值 = 计算之后 数据不安全
       # if while 条件 这两个判断是由多个线程完成的 数据不安全
import dis  
a = 0
def func():
   global a
   a += 1
'''
56           0 LOAD_GLOBAL             0 (a)
            2 LOAD_CONST               1 (1)
            4 INPLACE_ADD
            # GIL锁切换了
            6 STORE_GLOBAL             0 (a)
'''

dis.dis(func)  # 查看cup指令

 

2 线程锁
  • 最好只创建一把锁,线程锁一定锁不了进程

from threading import Thread,Lock

n = 0
def add(lock):
   for i in range(500000):
       global n
       with lock:
           n += 1


def sub(lock):
   for i in range(500000):
       global n
       with lock:
           n -= 1

t_l = []
lock = Lock()
for i in range(2):
   t1 = Thread(target=add,args=(lock,))
   t1.start()
   t2 = Thread(target=sub,args=(lock,))
   t2.start()
   t_l.append(t1)
   t_l.append(t2)
for t in t_l:
   t.join()
print(n)
from threading import Thread,Lock
import time
n = []
def append():
   for i in range(500000):
       n.append(1)
def pop(lock):
   for i in range(500000):
       with lock:
           if not n:
               time.sleep(0.0000001)    # 强制CPU轮转
           n.pop()

t_l = []
lock = Lock()
for i in range(20):
   t1 = Thread(target=append)
   t1.start()
   t2 = Thread(target=pop,args=(lock,))
   t2.start()
   t_l.append(t1)
   t_l.append(t2)
for t in t_l:
   t.join()
print(n)

# 不要操作全局变量,不要在类里操作静态变量
# += -= *= /= if while 数据不安全
# queue logging 数据安全的
3 单利模式加锁
import time
class A:
   from threading import Lock
   __instance = None
   lock = Lock()

   def __new__(cls, *args, **kwargs):
       with cls.lock:
           if not cls.__instance:
               time.sleep(0.000001)   # cpu轮转
               cls.__instance = super().__new__(cls)
       return cls.__instance

def func():
   a = A()
   print(a)
from threading import Thread
for i in range(10):
   Thread(target=func).start()
4 互斥锁和递归锁
from threading import Lock,RLock
# Lock 互斥锁   效率高   保证数据的安全 不可以多次acquire
# RLock 递归(recursion)锁 效率相对低 可以解决死锁的现象
# 在同一个线程中可以被acquire多次

# 互斥锁 : 在同一个进程中不能被连续acquire多次,一次acquire必须对应一次release
       # 相对效率高
# 递归锁 : 在同一个进程中可以被连续acquire多次,但一次acquire必须对应一次release
# 相对的效率低


from threading import Thread,RLock as Lock

def func(i,lock):
   lock.acquire()
   lock.acquire()
   print(i,': start')
   lock.release()
   lock.release()
   print(i, ': end')

lock = Lock()
for i in range(5):
   Thread(target=func,args=(i,lock)).start()
5 死锁现象
  • 死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。


# 死锁现象 : 多把锁,交替使用(两把锁,在第一把锁没有释放之前就获取第二把锁)
   # locka.acquire()
   # lockb.acquire()
# 怎么解决?
   # 最快的方法:把所有的互斥锁改成一把递归锁,影响效率
   # 后期再慢慢的整理逻辑,把递归锁能解决的问题用一把互斥锁解决掉,提高效率
import time
from threading import Thread,Lock,RLock
fork_lock = noodle_lock = RLock()
# fork_lock = RLock()

def eat(name):
   noodle_lock.acquire()
   print(name,'抢到面了')
   fork_lock.acquire()
   print(name, '抢到叉子了')
   print(name,'吃面')
   time.sleep(0.1)
   fork_lock.release()
   print(name, '放下叉子了')
   noodle_lock.release()
   print(name, '放下面了')

def eat2(name):
   fork_lock.acquire()
   print(name, '抢到叉子了')
   noodle_lock.acquire()
   print(name,'抢到面了')
   print(name,'吃面')
   noodle_lock.release()
   print(name, '放下面了')
   fork_lock.release()
   print(name, '放下叉子了')

Thread(target=eat,args=('alex',)).start()
Thread(target=eat2,args=('wusir',)).start()
Thread(target=eat,args=('taibai',)).start()
Thread(target=eat2,args=('大壮',)).start()
def eat(name):
   fork_noodle_lock.acquire()
   print(name,'抢到面了')
   print(name, '抢到叉子了')
   print(name,'吃面')
   time.sleep(0.1)
   fork_noodle_lock.release()
   print(name, '放下叉子了')
   print(name, '放下面了')

def eat2(name):
   fork_noodle_lock.acquire()
   print(name, '抢到叉子了')
   print(name,'抢到面了')
   print(name,'吃面')
   fork_noodle_lock.release()
   print(name, '放下面了')
   print(name, '放下叉子了')

Thread(target=eat,args=('alex',)).start()
Thread(target=eat2,args=('wusir',)).start()
Thread(target=eat,args=('taibai',)).start()
Thread(target=eat2,args=('大壮',)).start()


# 死锁现象是怎么产生的?
   # 多把(互斥/递归)锁 并且在多个线程中 交叉使用
       #     fork_lock.acquire()
       #     noodle_lock.acquire()
       #
       #     fork_lock.release()
       #     noodle_lock.release()
   # 如果是互斥锁,出现了死锁现象,最快速的解决方案把所有的互斥锁都改成一把递归锁
       # 程序的效率会降低的
   # 递归锁 效率低 但是解决死锁现象有奇效
   # 互斥锁 效率高 但是多把锁容易出现死锁现象
   # 一把互斥锁就够了
6 队列
import queue   # 线程之间数据安全的容器队列
from queue import Empty  # 不是内置的错误类型,而是queue模块中的错误
q = queue.Queue(4)   # fifo 先进先出的队列
# q.get() 是一个阻塞状态,等待取值
q.put(1)
q.put(2)
q.put(3)
q.put(4)
print('4 done')  # done 完成

# q.put_nowait(5) # 不会阻塞,容易丢数据一般不用
# q.put(5) # 也会阻塞,等待放值
# print('5 done')
try:
   q.get_nowait()  # 用这个方法可以检测出队列是否已满,不会丢数据
except Empty:pass
print('队列为空,继续其他内容')
from queue import LifoQueue   # last in first out 后进先出 栈
lq = LifoQueue()
lq.put(1)
lq.put(2)
lq.put(3)
print(lq.get())
print(lq.get())
print(lq.get())
from queue import PriorityQueue  # 优先级队列 放入数据的ascii码来从小到大输出的

priq = PriorityQueue()
priq.put((2,'alex'))
priq.put((1,'wusir'))
priq.put((0,'太白'))

print(priq.get())
print(priq.get())
print(priq.get())

5 池 concurrent.futrues

  • 什么是池 要在程序开始的时候,还没提交任务先创建几个线程或者进程,放在一个池子里,这就是池

  • 为什么要用池?

    如果先开好进程/线程,那么有任务之后就可以直接使用这个池中的数据了,并且开好的线程或者进程会一直存在在池中,可以被多个任务反复利用,这样极大的减少了开启\关闭\调度线程/进程的时间开销,池中的线程/进程个数控制了操作系统需要调度的任务个数,是控制池中的单位,有利于提高操作系统的效率,减轻操作系统的负担

  • 发展过程

  1. threading模块 没有提供池

  2. multiprocessing模块 仿照threading写的 Pool

  3. concurrent.futures模块 线程池,进程池都能够用相似的方式开启\使用

# 线程池
import time
import random
from threading import current_thread
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
def func(a,b):
   print(current_thread().ident,'start',a,b)
   time.sleep(random.randint(1,4))
   print(current_thread().ident,'end')

if __name__ == '__main__':
   tp = ThreadPoolExecutor(4)
   for i in range(20):
       tp.submit(func,i,b=i+1)  # 提交任务

# 实例化 创建池
# 向池中提交任务,submit 传参数(按照位置传,按照关键字传)
# 进程池
import os
import time,random
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
def func(a,b):
   print(os.getpid(),'start',a,b)
   time.sleep(random.randint(1,4))
   print(os.getpid(),'end')

if __name__ == '__main__':
   tp = ProcessPoolExecutor(4)
   for i in range(20):
       tp.submit(func,i,b=i+1)  # submit 提交任务
import os
import random
import time
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor


def func(a):
   print(os.getpid(),'start',a[0],a[1])
   time.sleep(random.randint(1,3))
   print(os.getpid(),'end')
   return a[0]*a[1]


if __name__ == '__main__':
   tp = ProcessPoolExecutor(4)
   ret = tp.map(func,((i,i+1)for i in range(10)))  #map 循环提交任务
   for i in ret:
       print(i)
# 获取任务结果
import os
import time,random
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
def func(a,b):
   print(os.getpid(),'start',a,b)
   time.sleep(random.randint(1,4))
   print(os.getpid(),'end')
   return a*b

if __name__ == '__main__':
   tp = ProcessPoolExecutor(4)
   futrue_l = {}
   for i in range(20):         # 异步非阻塞的
       ret = tp.submit(func,i,b=i+1)
       futrue_l[i] = ret
       # print(ret.result())   # Future未来对象
   for key in futrue_l:       # 同步阻塞的
       print(key,futrue_l[key].result())   # result() 获取结果
# 回调函数 : 效率最高的
import time,random
from threading import current_thread
from concurrent.futures import ThreadPoolExecutor

def func(a,b):
   print(current_thread().ident,'start',a,b)
   time.sleep(random.randint(1,4))
   print(current_thread().ident,'end',a)
   return (a,a*b)

def print_func(ret):       # 异步阻塞
   print(ret.result())

if __name__ == '__main__':
   tp = ThreadPoolExecutor(4)
   for i in range(20):         # 异步非阻塞的
       ret = tp.submit(func,i,b=i+1)
       ret.add_done_callback(print_func)  # ret这个任务会在执行完毕的瞬间立即触发print_func函数,并且把任务的返回值对象传递到print_func做参数
      # 异步阻塞 回调函数 给ret对象绑定一个回调函数,等待ret对应的任务有了结果之后立即调用print_func这个函数
      # 就可以对结果立即进行处理,而不用按照顺序接收结果处理结果
       
      # add_done_callback 回调函数
#进程池(高计算的场景,没有io(没有文件操作\没有数据库操作\没有网络操作\没有input)) : >cpu_count*1  <cpu_count*2
#   线程池(一般根据io的比例定制) : cpu_count*5
# 5*20 = 100并发
  • 应用

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import requests
import os

def get_page(url):    # 访问网页,获取网页源代码   线程池中的线程来操作
   print('<进程%s> get %s' %(os.getpid(),url))
   respone=requests.get(url)
   if respone.status_code == 200:
       return {'url':url,'text':respone.text}

def parse_page(res):   # 获取到字典结果之后,计算网页源码的长度,把https://www.baidu.com : 1929749729写到文件里   线程任务执行完毕之后绑定回调函数
   res=res.result()
   print('<进程%s> parse %s' %(os.getpid(),res['url']))
   parse_res='url:<%s> size:[%s]\n' %(res['url'],len(res['text']))
   with open('db.txt','a') as f:
       f.write(parse_res)


if __name__ == '__main__':
   urls=[
       'https://www.baidu.com',
       'https://www.python.org',
       'https://www.openstack.org',
       'https://help.github.com/',
       'http://www.sina.com.cn/'
  ]
   tp = ThreadPoolExecutor(4)
   for i in urls:
       ret = tp.submit(get_page,i)
       ret.add_done_callback(parse_page)

6 协程

  1. 是操作系统不可见的

  2. 协程本质就是一条线程 多个任务在一条线程上来回切换

  3. 利用协程这个概念实现的内容 : 来规避IO操作,就达到了我们将一条线程中的io操作降到最低的目的,节省io操作的时间也只能是和网络操作相关的

  4. 特点:数据安全,用户级别,开销小,不能利用多核,能够识别的io操作少

# 切换 并 规避io 的两个模块
# gevent = 利用了 greenlet   底层模块完成的切换 + 自动规避io的功能
# asyncio = 利用了 yield   底层语法完成的切换 + 自动规避io的功能
   # tornado 异步的web框架
   # yield from - 更好的实现协程
   # send - 更好的实现协程
   # asyncio模块 基于python原生的协程的概念正式的被成立
   # 特殊的在python中提供协程功能的关键字 : aysnc await

# 进程 数据隔离 数据不安全 操作系统级别 开销非常大 能利用多核 高密集型计算
# 线程 数据共享 数据不安全 操作系统级别 开销小     不能利用多核   一些和文件操作相关的io只有操作系统能感知到,单存计算模型
# 协程 数据共享 数据安全   用户级别     更小       不能利用多核   协程的所有的切换都基于用户,只有在用户级别能够感知到的io才会用协程模块做切换来规避(socket,请求网页的)

# 用户级别的协程还有什么好处:
   # 减轻了操作系统的负担
   # 一条线程如果开了多个协程,那么给操作系统的印象是线程很忙,这样能多争取一些时间片时间来被CPU执行,程序的效率就提高了

# a = 1
# def func():
#     global a
#     # 切换
#     a += 1
#     # 切换
#
# import dis
# dis.dis(func)
# 对于操作系统 : python代码--> 编译 --> 字节码 --> 解释 --> 二进制010101010010101010
# 二进制 反编译过来的 --> LOAD_GLOBAL

# 4cpu
# 进程 :5个进程
# 线程 :20个
# 协程 :500个
# 5*20*500 = 50000
import gevent

def func():    # 带有io操作的内容写在函数里,然后提交func给gevent
   print('start func')
   gevent.sleep(1)
   print('end func')

# g1 = gevent.spawn(func)
# g2 = gevent.spawn(func)
# g3 = gevent.spawn(func)
# gevent.joinall([g1,g2,g3])
# g1.join()   # 阻塞 直到协程g1任务执行结束
# g2.join()   # 阻塞 直到协程g1任务执行结束
# g3.join()   # 阻塞 直到协程g1任务执行结束



g1 = gevent.spawn(func)
g2 = gevent.spawn(func)
g3 = gevent.spawn(func)
gevent.joinall([g1,g2,g3])
import asyncio

async def func(name):  # async 标识一个函数是程函数
   print('start',name)
   # await 写好的asyncio中的阻塞方法
   # await 关键字必须写在一个async函数里
   await asyncio.sleep(1)
   print('end')

loop = asyncio.get_event_loop()# event_loop 事件循环:程序开启一个无限循环,把一些函数注册到事件循环上,当满足事件发生的时候,调用相应的协程函数
loop.run_until_complete(asyncio.wait([func('alex'),func('太白')]))
# 协程的原理
import time
def sleep(n):
   print('start sleep')
   yield time.time() + n
   print('end sleep')

def func(n):

   print(123)
   yield from sleep(n)   # 睡1s
   print(456)

def run_until_complete(g1,g2):
   ret1 = next(g1)
   ret2 = next(g2)
   time_dic = {ret1: g1, ret2: g2}
   while time_dic:
       min_time = min(time_dic)
       time.sleep(min_time - time.time())
       try:
           next(time_dic[min_time])
       except StopIteration:
           pass
       del time_dic[min_time]


n = 1
g1 = func(1)
g2 = func(1.1)
run_until_complete(g1,g2)
基于gevent实现socket并发
import socket
print(socket.socket)          # 在patch all之前打印一次
from gevent import monkey    # gevent 如何检测是否能规避某个模块的io操作呢?
monkey.patch_all() # 让gevent能够识别一些导入的模块中的io操作
import socket
import gevent
print(socket.socket)           # 在patch all之后打印一次,如果两次的结果不一样,那么就说明能够规避io操作
def func(conn):
   while True:
       msg = conn.recv(1024).decode('utf-8')
       MSG = msg.upper()
       conn.send(MSG.encode('utf-8'))

sk = socket.socket()
sk.bind(('127.0.0.1',9001))
sk.listen()

while True:
   conn,_ = sk.accept()
   gevent.spawn(func,conn)
import time
import socket
from threading import Thread
def client():
   sk = socket.socket()
   sk.connect(('127.0.0.1',9001))
   while True:
       sk.send(b'hello')
       msg = sk.recv(1024)
       print(msg)
       time.sleep(0.5)

for i in range(500):
   Thread(target=client).start()
posted @ 2021-03-26 09:01  Jack_Gao  阅读(109)  评论(0)    收藏  举报