并发编程--多进程

并发编程

并发与串行

就现在而言,我们写的程序还都是单一运行,一次只能运行一个程序这种运行方式是串行

  • 串行:程序自上而下进行运行,一行一行的顺序执行,只有把当前任务执行完成,才会执行下一个任务

例如:

  1. TCP服务器中,当正在进行通信循环时,就无法处理其他客户端的请求

  2. 当程序执行input时

  • 并行:可以同时执行多个任务,提高效率

串行和并行都时程序处理任务的方式

实现并发的方式

  1. 多线程
  2. 多进程
  3. 协程

进程是什么

进程是一个资源单位,指得是正在运行的程序,是操作系统调度以及进行资源分配的基本单位

进程是怎么来的?

​ 当把一个程序冲硬盘读入到内存时,进程就产生了

多进程

  • 多进程:指的是同一时间有多个程序被装入内存并执行

进程来自于操作系统,有操作系统进行调度以及资源分配

多进程的实现原理其实就是操作系统调度进程的原理

操作系统是什么

操作系统也是一个应用程序

操作系统的主要功能:

  1. 隐藏了硬件系统复杂的操作,提供了简单直观的API接口
  2. 将硬件的竞争变得,有序可控
  3. GUI 图形化用户界面

与普通程序的区别:

  1. 操作系统可以直接与硬件交互
  2. 操作系统是受保护的,不能直接被修改
  3. 操作系统更加长寿 一旦完成基本不会修改 例如系统内核

操作系统的发展史

  • 第一代计算机:真空管和穿孔卡片

    • 特点:

      1. 没有操作系统的概念

      2. 所有的程序设计都是直接操控硬件

    • 优点:程序员在申请的时间段内独享整个计算机,可以及时的调试自己的程序。

    • 缺点:浪费计算机资源,一个时间段内只有一个人用。

  • 第二代计算机:晶体管和批处理系统

    • 特点:

      1. 有了操作系统的概念
      2. 有了程序设计语言:FORTRAN语言或汇编语言,写到纸上,然后穿孔打成卡片,再将卡片盒带到输入室,交给操作员,等待输出接口
      3. 设计人员、生产人员、操作人员、程序人员和维护人员直接有了明确的分工,计算机被锁在专用空调房间中,由专业操作人员运行,这便是‘大型机’。
    • 优点:批处理,节省了机时

    • 缺点:

      1. 整个流程需要人参与控制

      2. 计算的过程仍然是顺序计算---->串行

      3. 程序员原来独享一段时间的计算机,现在必须被统一规划到一批作业中,等待结果和重新调试的过程都需要等同批次的其他程序都运作完才可以(这极大的影响了程序的开发效率,无法及时调试程序)

  • 第三代计算机:集成电路芯片和多道程序设计

    1. 使用SPOOLING联机技术

    2. 多道技术 (重点)

    3. 多终端多用户

    多道技术

    实现原理:

    1. 空间复用

      同一时间,加载多个任务到内存中,多个进程之间内存区域需要相互隔离,这种隔离是物理层面的隔离,其目的是为了保证数据安全

    2. 时间复用

      时间复用指的是,操作系统会在多个进程之间做切换执行

      • 切换任务的两种情况

        1. 当一个进程遇到了IO操作 时会自动切换

        2. 当一个任务执行时间超过阈值会强制切换

        注意:

        在切换前必须保存状态,以便后续恢复执行,并且 频繁的切换其实也需要消耗资源,当所有任务都没有IO操作时,切换执行效率反而降低,但是为了保证并发执行 必须牺牲效率

  • 第四代计算机:**大规模集成电路+多用户多终端系统 **

    体积降低,成本降低 , 发展出咯个人计算机即PC

    • 特点:

      大多具备GUI界面 , 即使是普通人不具备专业及技能也能流畅使用

并发编程中重要的概念

  • 描述处理任务的方式

    1. 串行:程序自上而下进行运行
    2. 并发:多个任务同时执行 ,但是本质是在不同进程间切换执行,由于速度非常快所以感觉是同时运行
    3. 并行:是真正的同时运行,必须具备多核CPU ,有几个核心就能并行几个任务,当任务数量超过核心数还是并发执行
  • 描述执行任务的方式

    1. 阻塞:指的是程序遇到了IO操作,无法继续执行代码时的一种状态
    2. 非阻塞:指的是程序没有遇到IO操作的一中状态

    input() 是默认的一个阻塞操作

    我们可以用一些手段将阻塞的操作变成非阻塞的操作 , 例如非阻塞的socket

  • 一个进程的三种状态

    阻塞

    运行

    就绪

进程的创建和销毁

对于通用计算机而言.必须具备创建和销毁进程的能力

  • 创建

    1. 用户的交互式请求 鼠标双击

    2. 由一个正在运行的程序 调用了开启进程的接口 . 例如subprocess

    3. 一个批处理作业开始

    4. 系统初始化

  • 销毁:

    1. 任务完成 自愿退出

    2. 强制结束 taskkill kill 非自愿

    3. 程序遇到了异常

    4. 严重错误 比如访问了不该访问的内存

进程和程序

  • 程序:一堆代码放在一个文件中,通常后缀为.exe,原本是存储在硬盘上的

  • 进程:将代码从硬盘读取到内存,然后执行产生的

    进程是由程序产生的 ,一个程序可以产生多个进程,例如qq多开,每一个进程都具备一个PID进程编号且是唯一的

进程的层次结构

​ 在linux中,进程具备父子关系,是一个树状结构,可以互相查找到对方

​ 在windows 没有层级关系 ,父进程可以将子进程的句柄转让

​ 父进程和子进程? 例如qq开启了浏览器,那么qq是父进程 ,浏览器是子进程

PID 和 PPID

  • PID:是当前进程的编号

  • PPID:是父进程的编号

    注意:当我们运行py文件时其实运行的是python解释器

    import os
    os.getpid()   # 访问PID
    os.getppid()  # 访问PPID
    

python如何使用多进程

  1. **导入multiprocessing 中的Process类 实例化这个类 指定要执行的任务 target **

    import os
    from multiprocessing import Process
    """
    Process 就表示进程
    """
    
    def task():
        print("this is sub process")
        print("sub process id %s" % os.getpid())
    
    
    if __name__ == '__main__':
        # 注意 开启进程的代码必须放在 ————main————判断下面
        # 实例化一个进程对象 并制定他要做的事情  用函数来指定
        p = Process(target=task)
        p.start() # 给操作系统发送消息 让它开启进程
        print("this is parent process")
        print("parent process is: %s" % os.getpid())
        print("over")
    
    • 注意:

      linux 与windows开启进程的方式不同 ,linux 会将父进程的内存数据 完整copy一份给子进程 ,而windows 会导入父进程的代码 从头执行一遍 来获取需要处理的任务

      所以在编写代码时如果是windows一定要将开启进程的代码放main判断中

  2. **导入multiprocessing 中的Process类 继承这个类 覆盖run方法 将要执行的任务放入run中开启进程时会自动执行该函数 **

    from multiprocessing import Process
    import os
    
    
    class Downloader(Process):
        
        def run(self):
            print(os.getpid())
            pass
    
    if __name__ == '__main__':
        m = Downloader()
        m.start()
        print("parent over",os.getpid())
    

    如果需要对进程对象进行高度自定义那就可以继承它

  3. 进程之间的内存相互隔离

    from multiprocessing import  Process
    import os,time
    
    a = 257
    
    
    def task():
        global a
        # print("2",a,id(a))
        a = 200
        print(a)  # 200
         
    
    if __name__ == '__main__':
        p = Process(target=task)
        p.start() # 向操作系统发送指令
    
        time.sleep(4)
        print(a)  # 257
    
  4. join函数

    join函数就是让主程序进入等待状态,需要子程序执行完成后才能继续执行

    from multiprocessing import Process
    import time
    def task1(name):
        for i in range(10000):
            print("%s run" % name)
    
    def task2(name):
        for i in range(100):
            print("%s run" % name)
    
    if __name__ == '__main__': # args 是给子进程传递的参数 必须是元组
        p1 = Process(target=task1,args=("p1",))
        p1.start()  # 向操作系统发送指令
        # p1.join()   # 让主进程 等待子进程执行完毕在继续执行
    
        p2 = Process(target=task2,args=("p2",))
        p2.start()  # 向操作系统发送指令
    
        p2.join()  # 让主进程 等待子进程执行完毕在继续执行
        p1.join()
    
    
        #需要达到的效果是 必须保证两个子进程是并发执行的 并且 over一定是在所有任务执行完毕后执行
        print("over")
    
    # join的使用
    from multiprocessing import Process
    import time
    def task1(name):
        for i in range(10):
            print("%s run" % name)
    
            
    if __name__ == '__main__': # args 是给子进程传递的参数 必须是元组
    
    	# 实现了10个进程同时运行,并且要等10个子程序运行完了之后才能运行主程序
        ps = []
        for i in range(10):
            p = Process(target=task1,args=(i,))
            p.start()
            ps.append(p)
    
        # 挨个join以下
        for i in ps:
            i.join()
    
        print("over")
    

进程对象的常用属性

if __name__ == '__main__':
    p = Process(target=task,name="老司机进程")
    p.start()
    p.join() # 让子程序先运行
    print(p.name) # 打印进程名字
    p.daemon #守护进程
    print(p.exitcode) # 获取进程的退出码   就是exit()函数中传入的值
    print(p.is_alive())  # 查看进程是否存活
    print("zi",p.pid) # 获取进程id
    print(os.getpid())
    p.terminate()  #终止进程  与strat 相同的是 不会立即终止,因为操作系统有很多事情要做  

僵尸进程与孤儿进程

  • 孤儿进程:当父进程已经结束,而子进程还在运行,子进程就称为孤儿进程,有其存在的必要性,没有不良影响

  • 僵尸进程:当一个进程已经结束了,但是它仍然还有一些数据存在,此时称之为僵尸进程

    在linux中,有这么一个机制,父进程无论什么时候都可以获取到子进程的的一些数据

    子进程任务执行完毕后,确实结束了但是仍然保留一些数据,目的是为了让父进程能够获取这些信息

    **linux中可以调用waitpid来是彻底清除子进程的残留信息 **

    **python中已经封装了处理僵尸进程的操作 ,无需关心 **

守护进程

什么是守护进程

  • 在python中,守护进程也是一个进程,默认情况下,主进程即使代码执行完毕,但子进程还没结束时,主进程会等待子进程执行完毕之后才会关闭

    也就是说,当a是b的守护进程时,a是守护进程,b是被守护进程

    当b进程结束时,即使a进程未完成也会被结束

    from multiprocessing import Process
    import time
    
    def task():
      print('zi run')
      time.sleep(3)
      print('zi over')
    
      if __name__ == "__main__":
          p = Process(target=task)
          p.daemon = True  # 将这个进程设置为了守护进程  必须在开启进程前设置
          p.start()
          # time.sleep(4)
          print('zhu over')
    
    # 有time.sleep(4)
    zi run
    zi over
    zhu over
    
    # 无time.sleep(4)
    zhu over
    

进程安全问题

当并发的多个任务,同时操作同一个资源,就会造成数据错乱的问题

解决办法:将并发操作公共资源的代码,由并发变为串行,这样就解决了安全问题,但是牺牲了效率

  • 串行的方式

    1. 使用join函数

      强行把并行改为串行,不如不开进程

      多个进程之间原本公平竞争 join是强行规定了执行顺序

    2. 互斥锁*****

      原理:就是把需要操作公共资源的代码锁起来,保证同一时间只能有一个进程执行这部分代码

互斥锁是什么

互斥锁是一个相互排斥的锁

  • 优点:可以将部分代码串行

  • 注意:必须保证锁只有一把

使用方式

from multiprocessing import Process, Lock
import time, random


def task1(mutex):
    for i in range(100):
        print(1)

    mutex.acquire()  # 加锁
    time.sleep(random.random())
    print('--------1')
    time.sleep(random.random())
    print('--------2')
    mutex.release()  # 解锁


def task2(mutex):
    for i in range(100):
        print(5)

    mutex.acquire()  # 加锁
    time.sleep(random.random())
    print('+++++++++1')
    time.sleep(random.random())
    print('+++++++++2')
    mutex.release()  # 解锁


if __name__ == '__main__':
    mutex = Lock()

    p1 = Process(target=task1, args=(mutex,))
    p2 = Process(target=task2, args=(mutex,))
    p1.start()
    p2.start()

添加互斥锁,虽然解决了安全问题,但是带来了效率降低的问题

锁其实只是给执行代码增加了限制,其本质是一个标志,就是True或False

lock = False


def task1():
    global lock
    if lock == False:
        lock = True
        open("aaa.txt", "wt")
        lock = False


def task2():
    global lock
    if lock == False:
        lock = True
        open("aaa.txt", "wt")
        lock = False
  • 如何保持安全又提高效率呢?

    粒度:一个资源单位,指的是被锁住的代码多少

    锁的粒度越小,效率越高

互斥锁案例

# 抢票系统
import json
from multiprocessing import Process, Lock


def show():
   with open('db.json', 'r', encoding='utf-8') as fr:
       data = json.load(fr)
       print(f'剩余票数为{data["count"]}')


def buy():
   with open('db.json', 'r', encoding='utf-8') as fr:
       data = json.load(fr)
       if data['count'] > 0:
           data['count'] -= 1
           with open('db.json', 'w', encoding='utf-8') as fw:
               json.dump(data, fw)
               print('抢票成功')
       else:
           print('抢票失败')


def task(mutex):
   show()
   mutex.acquire()
   buy()
   mutex.release()


if __name__ == '__main__':
   mutex = Lock()
   for i in range(5):
       p = Process(target=task, args=(mutex,))
       p.start()

IPC--Inter-Process Communication

  • IPC指的是进程间通讯

我们开多进程就是通过多道技术的空间复用方法,开辟了多个相互隔离的内存空间,这样使得进程之间不能进行交互

进程间通讯方法

  1. 创建一个共享文件

    • 适用于交互不频繁且数据量较大的情况

    优点:理论上可以交互的数据量没有限制

    缺点:交互效率低

  2. 共享内存(主要方式)

    • 适用于交互频繁但数据量较小的情况

    优点:效率高

    缺点:交互数据量较小

  3. 管道

    • 它是基于文件的,而且是单向的,编程比较复杂
  4. socket

    • 更加适用于基于网络进行通讯,交换数据

共享内存的方式

  • 第一种

    Manager

    创建一个进程间同步的容器,但是没有处理安全问题,所以不常用

  • 第二种

    Queue可以帮我们完成进程间通讯

    Queue是一个队列,一种特殊的容器,存取顺序为先进先出

    from multiprocessing import Queue
    
    q = Queue(2) # 创建队列 并且同时只能存储2个元素
    q.put(1)
    q.put(2)
    
    # q.put(3,block=True,timeout=3) # 默认是阻塞的 当容器中没有位置了就阻塞 直到有人从里面取走元素为止
    print(q.get())
    print(q.get())
    print(q.get(block=True,timeout=3))# 默认是阻塞的 当容器中没有位置了就阻塞 直到有人存入元素为止
    

    扩展

    • 栈:也是一种特殊的容器,但是存取顺序为先进后出

    函数的调用栈,遵循以什么函数开头,就要以什么函数结束

    调用函数时 称之为 函数入栈

    函数执行结束 称之为函数出栈

posted @ 2019-07-02 21:03  蔚蓝的爱  阅读(274)  评论(0编辑  收藏  举报