zoukankan      html  css  js  c++  java
  • 多进程和多线程

    进程的概念

    进程:一个正在执行的程序

    • 计算机程序是存储在磁盘上的可执行二进制(或其他类型)文件,只有把它们加载到内存中,并被操作系统调用,它们才会拥有其自己的生命周期。
    • 进程是表示的一个正在执行的程序。
    • 每个进程都拥有自己的地址空间、内存、数据栈以及其他用于跟踪执行的辅助数据。
    • 操作系统负责其上所有进程的执行,并为这些进程合理地分配执行时间。
    • 进程之间是独立的,不能共享彼此的数据。

    并行与并发

    并行:同时进行多个任务
    并发:短时间内,运行多个任务

    并发:单CPU,多进程并发

         无论是并行还是并发,在用户看来都是'同时'运行的,不管是进程还是线程,都只是一个任务而已,真实干活的是cpu,cpu来做这些任务,而一个cpu同一时刻只能执行一个任务

          一 并发:是伪并行,即看起来是同时运行。单个cpu+多道技术就可以实现并发,(并行也属于并发)

    并行:多CPU(同时运行,只有具有多个cpu才能实现并行)

     

    并行满足的条件是总进程数量不多于 CPU核心数量!因此,现在PC运行的程序大部分都是轮询调度产生的并行假象

    举例:比如PC有四个核,六个任务,这样同一时间有四个任务被执行,假设分别被分配给了cpu1,cpu2,cpu3,cpu4,

       一旦任务2遇到I/O就被迫中断执行,此时任务5就拿到cpu1的时间片去执行,这就是单核下的多道技术

       而一旦任务2的I/O结束了,操作系统会重新调用它(需知进程的调度、分配给哪个cpu运行,由操作系统说了算),可能被分配给四个cpu中的任意一个去执行

      所以,现代计算机经常会在同一时间做很多件事,一个用户的PC(无论是单cpu还是多cpu),都可以同时运行多个任务(一个任务可以理解为一个进程)。

    多道技术:内存中同时存入多道(多个)程序,cpu从一个进程快速切换到另外一个,使每个进程各自运行几十或几百毫秒,这样,虽然在某一个瞬间,一个cpu只能执行一个任务,但在1秒内,cpu却可以运行多个进程,这就给人产生了并行的错觉,即伪并发,以此来区分多处理器操作系统的真正硬件并行(多个cpu共享同一个物理内存)

    例子:在python中执行耗时操作

     结果:

    用进程来分担耗时任务后:

    python进程使用流程

    线程的概念

    • 线程被称作轻量级进程。
    • 与进程类似,不过它们是在同一个进程下执行的。
    • 并且它们会共享相同的上下文。
    • 当其他线程运行时,它可以被抢占(中断)和临时挂起(也称为睡眠)
    • 线程的轮训调度机制类似于进程的轮询调度,只不过这个调度不是由操作系统来负责,而是由Python解释器来负责。

    GIL锁

    Python在设计的时候,还没有多核处理器的概念。因此,为了设计方便与线程安全,直接设计了一个锁。这个锁要求,任何进程中,一次只能有一个线程在执行。
    因此,并不能为多个线程分配多个CPU。所以Python中的线程只能实现并发,而不能实现真正的并行。但是Python3中的GIL锁有一个很棒的设计,在遇到阻塞(不是耗时)的时候,会自动切换线程。

    Scrapy、Django、Flask、Web2py ...等框架 都是使用多线程来完成
    GIL锁带给我们的新认知,遇到阻塞就自动切换。因此我们可以利用这种机制来有效的避开阻塞 ,充分利用CPU

    例子:使用线程来避开阻塞任务

    python线程使用流程

     

    计算密集型和IO密集型

    IO密集型遇到等待时,不消耗CPU,CPU会调度其他程序执行,使用多线程可以有效的进行并发(爬虫的请求是IO密集型),线程在系统中所占资源较少。
    计算密集型:CPU会一直计算,此时使用多线程,反而会耗时更多,此时使用多进程(CPU越多,性能越好)。

    使用多进程/多线程实现并发服务器:

    import socket
    from multiprocessing import Process
    from threading import Thread
    
    
    def read(conn,addr):
        while True:
            data = conn.recv(1024)
            if data:
                print('客户端{}的消息:{}'.format(addr,data.decode()))
                conn.send(data)
            else:
                conn.close() 
                print('客户端{}断开连接'.format(addr))
                break
    
    
    if __name__ == '__main__':
        server = socket.socket()
        server.bind(('', 9998)) # 注意这些代码要写在if...main...判断下面,因为在Windows中,使用进程的模式类似导入,在判断外面的代码会在子进程执行
        server.listen(5)
        while True:
            print('等待客户端连接')
            conn,addr = server.accept()  # 有客户端连接,就往下进行
            print('客户端{}已连接'.format(addr))
            # p = Process(target=read,args=(conn,addr))  # 使用多进程,每来一个客户端连接,就分出一个进程去和它一对一处理
            # p.start()
            t = Thread(target=read,args=(conn,addr))  # 使用多线程,每来一个客户端连接,就分出一个线程去和它一对一处理
            t.start()

    补充

    fork介绍

    Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。

    子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。

    import os
    
    if __name__ == '__main__':
    
        print('进程 (%s) start...' % os.getpid())
        pid = os.fork()
        # time.sleep(10)
        if pid == 0:  # 若fork()返回0为子进程
            print("子进程{},父进程{}".format(os.getpid(), os.getppid())) #getpid()获取当前进程的pid,getppid()获取父进程的pid
        else:  # 父进程fork返回子进程的id
            print("父进程{},子进程{}".format(os.getpid(), pid))
    
    >>
    进程 (3130) start...
    父进程3130,子进程3131
    子进程3131,父进程3130

    multiprocessing模块介绍

    python中的多线程无法利用CPU资源,在python中大部分计算密集型任务使用多进程。如果想要充分地使用多核CPU的资源(os.cpu_count()查看)

    python中提供了非常好的多进程包multiprocessing。

    multiprocessing模块用来开启子进程,并在子进程中执行功能(函数),该模块与多线程模块threading的编程接口类似。

    multiprocessing的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。

     计算密集型任务:如金融分析,科学计算

    multiprocessing.current_process() :在任何一个进程中搞清楚自己是谁,在线程中也有类似方法

    Process类的介绍

    1.创建进程的类

    Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)
    
    强调:
    1. 需要使用关键字的方式来指定参数
    2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号

    2.参数介绍

    group参数未使用,值始终为None
    
    target表示调用对象,即子进程要执行的任务
    
    args表示调用对象的位置参数元组,args=(1,2,'al',)
    
    kwargs表示调用对象的字典,kwargs={'name':'al','age':18}
    
    name为子进程的名称

    3.方法介绍

    p.start():启动进程,并调用该子进程中的p.run() 
    p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法  
    
    p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。
    如果p还保存了一个锁那么也将不会被释放,进而导致死锁,线程中无此方法
    p.is_alive():如果p仍然运行,返回True
    
    p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)再往下运行。timeout是可选的超时时间,
    需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程

    4.属性介绍

    p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
    
    p.name:进程的名称
    
    p.pid:进程的pid,线程中是t.ident 注意只有进程或线程已经启动才会分配pid或ident
    
    p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)
    
    p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,
    这类连接只有在具有相同的身份验证键时才能成功(了解即可)

    以用面向对象的形式使用进程与线程

    关键点:

    • 继承Process或Thread类
    • 重写__init__方法
    • 重写run方法
    from multiprocessing import Process
    import time
    import random
    import os
    class pro(Process): #继承Process类 def __init__(self,name): super().__init__() # 重写__init__方法 这里是直接调用父类的初始化方法 self.name = name
    def run(self): # 重写run方法 print('%s is working,父进程%s,当前进程%s'%(self.name,os.getppid(),os.getpid())) time.sleep(1) print('%s end'%self.name)

    if __name__ =='__main__': p1 = pro('a') p2 = pro('b') p3 = pro('c') p1.start() p2.start() p3.start() print('主进程',os.getpid()) >> 主进程 9116 b is working,父进程9116,当前进程4316 a is working,父进程9116,当前进程6664 c is working,父进程9116,当前进程10132 b end a end c end

     总结:

    Windows用的导入(所以if...main...判断外面的代码,在子进程仍会执行)
    
    方法:
    p = Process(target=func,name='p1',daemon=True) #实例的时候可以指定方法、名字、是否守护进程
    p.current_process() 获取当前进程对象
    p.is_alive() 判断进程实例是否在运行 是返回True 否返回False
    p.terminate() 结束进程 线程无此方法
    p.name 获取进程名字
    p.join() 等待子进程结束再往下执行
    p.daemon = True 把进程设为守护进程 (父进程结束,守护进程也结束)
    注意:如果设置了join() 那么terminate和daemon 就不管用啦
    p.pid 获取当前进程的pid  线程是t.ident  
    注意:只有进程(或线程)启动之后,操作系统(或python解释器)才会分配pid(或ident)
    
    默认在子进程当中,会关闭标准输入
    import sys
    import os
    sys.stdin = os.fdopen(0) #加上这行代码,子进程才能使用标准输入
    
    多线程的方法与属性
    t.setDaemon(True)
    t.setName('p1')
    t.getName()
  • 相关阅读:
    windchill系统——一些功能查找
    HTML常用标签——思维导图
    windchill系统——导航器v1.0:思维导图
    IOS动画总结
    面试 必备
    iOS 数据库操作(使用FMDB)
    IOS 面试
    iOS中常用的四种数据持久化方法简介
    数据持久化的复习
    多线程---代码
  • 原文地址:https://www.cnblogs.com/woaixuexi9999/p/9323712.html
Copyright © 2011-2022 走看看