Python系统编程笔记

时间:2022-03-13 06:07:06

01. 进程与程序
  编写完毕的代码,在没有运行的时候,称之为程序
  正在运行着的代码,就称为进程
  进程是系统分配资源的最小单位。
  进程资源包括:
    中间变量
    代码
    计数器

02. 通过os.fork()函数在程序中创建进程
  示例:
    import os
    import time
    ret = os.fork() # 创建新的进程 一次调用,两次返回
    if ret == 0:
      for i in range(3):
        print("放音乐")
        time.sleep(0.1)
    else:
      for i in range(3):
      print("跳舞")
      time.sleep(0.1)
  说明:
    1. 程序执行到os.fork()时,操作系统会创建一个新的进程(子进程),然后复制父进程的所有信息到子进程中
    2. 然后父进程和子进程都会从fork()函数中得到一个返回值,在子进程中这个值一定是0,而父进程中是子进程的pid号
    3. 该函数为操作系统提供只能在Unix/Linux操作系统中使用,而不能在windows中使用。

03. 获取进程编号
  1. Python中的方法
    os.getpid() 获取当前进程的pid
    os.getppid() 获取当前进程的父进程的pid
  2. Linux中的方法
    ps -aux 获取当前进程的pid
    ps -ef 可以查看到父进程的pid

04. 资源回收
  01分配给子进程的资源由父进程负责回收
    倘若子进程没有运行完,父进程运行结束退出,留下的子进程被称为孤儿进程。父进程死掉后,孤儿进程会被别的进程收养,通常是init进程(pid为1),资源会被继父进程回收,所以没什么危害.
倘若子进程先运行完,而父进程并没有回收资源,那么这个子进程就被称为僵尸进程。僵尸进程会造成资源浪费。
  02. 父进程回收子进程资源的方法
    pid, result = os.wait()
      返回值pid表示回收的子进程的pid
      result表示子进程的退出状态,通常0表示正常运行完退出
      该函数是阻塞方式运行,直到子进程运行结束

05. 多个进程修改全局变量的问题
  由于子进程的资源来源于父进程的拷贝,所以父子进程都分配有自己的存储空间,在进程修改全局变量,并不会影响另一个进程内变量的值。

06. 通过Process类创建进程
  01. 介绍
    Process类位于multiprocessing模块中,是Python封装好的可以跨平台的类。
  02. 创建对象
    p = Process(target=fn, args=(),kwargs={},name="")
      target参数表示创建的进程要执行的函数
      args参数表示fn函数所需要的参数
      kwargs表示fn函数所需要的关键字参数
      name参数为当前创建的进程起一个别名,可以通过p.name访问得到
  03. Process对象常见的属性和方法
    p.name 获取进程的别名
    p.pid 获取进程的pid
    p.start() 真正创建出子进程,启动子进程
    p.is_alive() 查看子进程是否存活,返回True或者False
    p.terminate() 无论子进程是否执行完,立即终止。存在延迟,这是因为子进程创建后就作为一个单独的进程,要想在父进程中结束,需要父进程告诉操作系统,操作系统在下次调度子进程时结束掉子进程
    p.join() 回收子进程的资源,阻塞,指导子进程执行完
  04. 示例
    from multiprocessing import Process
    import os
    import time
    def sub_process_fun(num, a):
      for i in range(10):
        print("子进程:hello")
        time.sleep(0.1)
    def main():
      p = Process(target=sub_process_fun, args=(100,), kwargs={"a": 200}) # 创建一个子进程对象
      p.start() # 真正的创建出子进程,子进程可以开始执行代码
      p.join() # 回收子进程资源 阻塞
if __name__ == '__main__':
  main()

07. 通过继承Process类创建进程
  创建一个类继承自Process类,然后把子进程要执行的任务放在run方法中即可。
  示例:
    class SubProcess(Process):
      def __init__(self, num, a):
        super(SubProcess, self).__init__() # 执行父类Process默认的初始化方法,通过父类的方法,将子进程对象初始化好
        self.num = num
        self.a = a

      def run(self):
        """子进程要执行的代码"""
        print("子进程:pid=%d" % os.getpid())
        print("子进程:num=%d" % self.num)
        print("子进程:a=%d" % self.a)
        for i in range(10):
          print("子进程:hello")
          time.sleep(0.1)

    def main():
      p = SubProcess(100, 200)
      p.start() # 真正的创建出子进程,子进程可以开始执行代码
      time.sleep(0.1)
      p.terminate() # 终止子进程的执行 存在延迟
      p.join() # 回收子进程资源 阻塞
    if __name__ == '__main__':
      main()

08. 进程池
  01. 介绍
    当程序中需要大量创建进程时就可以使用进程池来维护这些进程
    进程池中会创建指定数量的进程,每次交给进程池任务后,进程池会
    自动选择一个空闲的进程来运行交给的任务。
    Python中的进程池通过使用multiprocessing模块的Pool类来实现
  02. 创建进程池对象
    pool = Pool(3)
      参数3表示创建的进程池中最大进程数
  03. 进程池对象的常见方法和属性
    01. pool.apply_async(fn, args, callback)
      用空闲出来的子进程去调用目标fn,非阻塞
      fn 表示需要进程执行的任务函数
      args 表示fn函数的参数,是一个元组
      callback 异步任务结束后会自动调用callback参数对应的函数,同时把异步任务fn的返回值传递给callback的参数。callback是由主进程调用的。
    02. pool.apply(fn, args)
      用空闲出来的子进程去调用目标fn,阻塞
    03. pool.close()
      关闭进程池,关闭后po不再接收新的请求
    04. pool.join()
      等待po中所有子进程执行完成,必须放在close语句之后,阻塞方式
  04. 注意点
    应该先定义任务,然后定义进程池对象。这是因为定义进程池对象的时候会
    创建子进程,同时把父进程的资源复制一份给子进程,倘若此时任务函数还
    没有定义,子进程在调用函数的时候就会出现找不到的情况。

09. 异步 同步
  同步: 从多任务的角度出发,多个任务之间执行的时候要求有先后顺序,必须一个先执行完成之后,另一个才能继续执行, 只有一个主线。即子线程执行的时候主线程会停止等待。
  异步: 从多任务的角度出发,多个任务之间执行没有先后顺序,可以同时运行,执行的先后顺序不会有什么影响,存在的多条运行主线。即主线程和子线程可以同时执行。
  阻塞: 从调用者的角度出发,如果在调用的时候,被卡住,不能再继续向下运行,需要等待,就说是阻塞
  非阻塞: 从调用者的角度出发, 如果在调用的时候,没有被卡住,能够继续向下运行,无需等待,就说是非阻塞
  同步和异步是从整个过程中间是否有等待的角度来看
  阻塞和非阻塞是从拿到结果之前的状态的角度来看。

10. Queue类
  01. 介绍
    multiprocessing类的Queue类,提供队列的功能,该功能由操作系统提供,由Python进行封装而成
    Queue对象不能当做一个普通的变量来看待,由于它是由操作系统提供,数据并不保存在进程内部,进程内的变量只是该对象的引用,所以可以实现多个进程间的通信。
  02. 使用
    1. q = Queue(3) #
      构造一个Queue对象,3表示可以向队列中put3条消息,如果不传参数,则表示可以put任意条消息。
    2. q.put(obj, timeout=3)
      往队列中添加数据。如果没有timeout参数,并且队列已经满了,则会出现阻塞直到队列中有空闲位置。
      timeout表示最长等待时间。如果队列中数据已满,则最多等待3秒,如果3秒内不能添加数据,则抛出异常
    3. obj = q.get(timeout=n)
      从队列中取数据,如果队列中没有数据,则等待n秒,n秒内没有取到数据,则会抛出异常
      如果没有timeout参数,则会阻塞在这里一直等待直到队列中有数据。
    4. q.task_done() 一般配合get使用,让队列计数减1
    4. q.put_nowait()
      向队列中添加数据,如果队列已满,则立即抛出异常
    5. obj = q.get_nowait()
      从队列中取数据,如果队列中没有数据,则立即抛出异常
    6. q.full()
      查看队列是否已满 返回True或者FALSE
    7. q.empty()
      查看队列是否是空的。返回True或者False
    8. q.qsize()
      当前队列中数据的个数。mac中不支持。
    9. q.join()
    多线程中,让主线程等待队列中的事情做完然后再退出。
  03. 通过Queue对象实现进程间通讯
    queue = Queue(3) # 参数表明这个队列最多只能保存3条数据

    def process_write():
      for i in range(3):
        queue.put(i)
        time.sleep(0.5)

    def process_read():
      while not queue.empty():
        ret = queue.get()
        time.sleep(0.1)

    p1 = Process(target=process_write)
    p2 = Process(target=process_read)
    p1.start()
    p1.join()
    p2.start()
    p2.join()

  04. 进程池中进程之间的通讯,不能使用Queue类,而是使用multiprocessing模块提供的Manager类
    对象的Queue()方法来创建一个队列对象。其他使用规则一样。

11. 线程介绍
  可以简单理解为同一进程中有多个计数器
  线程内每个线程的执行时间不确定,而每个进程的时间片相等
  线程是操作系统调度执行的最小单位

12. 进程线程的区别与优缺点
  1. 定义的不同
    进程是系统进行资源分配的最小单位.

    线程是进程的一个实体,是CPU进行调度的基本单位,
    它是比进程更小的能独立运行的基本单位.
    线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),
    但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

  2. 区别
    一个程序至少有一个进程,一个进程至少有一个线程.

    线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。

    进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率

    线线程不能够独立执行,必须依存在进程中

  3. 优缺点
    线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。

13. 多线程的实现
  通过threading模块的Thread类来实现多线程
    1. 通过实例化一个Thread类的一个对象来实现
    2. 通过继承Thread类来实现
    3. 实现的所有方法都可Process类的一样,可以参考Process类

14. 获取线程信息
  通过threading模块的enumerate()方法在主线程中获取所有线程信息
  返回一个列表,每一个元素是一个线程对象,保存一个线程的基本信息

  threading.current_thread()
    获取当前线程信息

15. 线程的几种状态
  就绪状态: 创建一个线程对象,调用start方法后,线程就处于就绪状态
  运行状态: 操作系统调度该线程时,线程就处于运行状态。
  等待(阻塞): 当线程运行中遇到阻塞,操作系统就会立即切换到别的线程去执行,同时这个线程进入阻塞状态,等待阻塞结束后,重新进入就绪状态,等待操作系统下次调度。所以线程的执行时间不均等。
  死亡状态: 线程调度运行结束后进入死亡状态

16. 注意点
  对于通过Thread类或者Process类实现的多线程或者多进程,
  如果主线程(进程)没有执行join操作,则主线程(进程)执行完也会
  等待子线程(进程)执行完后一块退出。
  这是由于Python封装实现的,而底层的os.walk()则不会这样

17. 多线程修改全局变量
  由于多个线程共享同一进程内的资源,
  所以多线程修改全局变量会出现资源竞争的问题。

18. 使用互斥锁解决多线程资源竞争问题
  1. 通过使用threading模块的Lock类来实现
  2. Lock类对象的使用方法
    1. mutex = Lock()
      创建一个互斥锁对象
    2. mutex.acquire(blocking=True, timeout=None)
      给互斥锁上锁
      blocking表示是以阻塞或者非阻塞的方式运行上锁
        True 默认 以阻塞的方式运行
          首先判断当前互斥锁的状态,
          如果是上锁状态,则阻塞等到锁为未上锁状态,由于发生阻塞,操作系统会立即切换到别的线程执行
          如果是未上锁状态,则将互斥锁设置为上锁状态
          总是返回True
          如果指定了timeout参数,则表示最长等待时间,超过这个时间仍未上锁成功,就不再等待,直接返回一个false值
      FALSE 表示以非阻塞方式运行该方法
        运行该方法会直接返回结果,如果上锁成功,则返回True,上锁失败,返回FALSE

      注:
        非阻塞方式可以通过对返回值的判断而知道是否上锁成功,这样就可以在上锁不成功的情况
        下做一些其他的事情而不用总是等待在这里。
    3. mutex.release()
      释放锁
      将互斥锁的状态设置为打开。

19. 多线程修改局部变量
  通过继承Thread类得到一个子类,
  然后通过该子类构造多个对象,让这几个对象同时开启子线程
  或者定义一个函数,多个线程同时运行该函数,
  通过在函数内修改函数的局部变量或者子类的run方法内修改对象的属性,
  多个线程直接不会互相影响修改的结果。

20. 线程死锁
  1. 起因
    线程中存在多个互斥锁,多个线程都需要对方的资源即每个线程都需要对方的锁先打开,就会出现死锁现象

  2. 避免死锁的方法
    1. 添加超时时间
      给acquire方法添加timeout参数,过了超时时间还拿不到锁,就把自己的互斥锁释放掉

    2. 银行家算法
      该算法是从资源分配者的角度出发,
      先把资源分配给需求最少的客户,等这个客户用完把所有资源归还后,
      再分配给需求大一点的客户

21. 同步应用(多个线程按照一定的顺序执行)
  通过互斥锁来控制线程的运行顺序
  即一个线程运行完并不释放自己的互斥锁,
  而是释放下一个线程要用到的互斥锁,
  下一个线程就会得到互斥锁,
  执行完后也不释放自己的互斥锁,同样释放下一个想要执行的线程的互斥锁,
  最后一个线程释放第一个线程的锁。
  程序开始时,把第一个要执行的线程的互斥锁打开,其他的全部上锁。

22. 生产者消费者模式
  所谓生产者消费者模式,放在线程中就是一个线程用来产生数据,
  而另一个线程用来处理数据。同时为了减少这两个线程之间的耦合度,
  可以引入一个仓库,生产者和消费者都操作这个仓库。
  仓库可以使用queue模块的Queue类来模拟,使用方法和multiprocessing模块的Queue类一样。

23. GIL(全局解释器锁)
  全局解释器锁导致Python中的多线程并不能同时运行
  在Python中创建一个子线程相当于在Python解释器的进程
  中创建一个线程,而所有的代码要运行都需要用到解释器的一些全局资源,
  而为了让每个线程都能安全的运行,就出现了GIL,每个线程或者每段代码在运行的时候都需要拿到GIL才能运行,
  这就让Python的多线程并不是真正的多个线程在同时执行,而是交替进行的。
  当然这里所说的子线程或者代码是指计算型的代码,涉及到io型的代码并不需要拿到GIL就可以执行,因为io操作一般都是调用底层C语言的代码,
  调用之前就会自动释放掉GIL锁,这个时候其他的线程就可以拿到GIL来运行
  所以在涉及IO操作的部分,可以使用多线程来提高效率。
  GIL问题只有在C语言实现的cpython解释器中有,别的版本的Python解释器如pypy,jpython中并没有这个问题。

24. 多线程多进程的使用地方
  一般CPU密集型即需要大量计算的时候考虑使用多进程以充分利用CPU
  IO密集型即文件读写,网络访问,数据库访问时考虑使用多线程。