美文网首页
4.从linux到python:线程和进程

4.从linux到python:线程和进程

作者: celusing | 来源:发表于2022-10-11 14:19 被阅读0次

    linux进程和线程:https://www.cnblogs.com/cxuanBlog/p/13277369.html

    一.Linux进程和线程

    1.进程和线程的区别

    • 进程是系统资源分配的最小单位,线程是系统调度的最小单位
    • 进程在初始化的时候,就会拥有一个独立的控制线程

    https://blog.csdn.net/weixin_44602505/article/details/110893949
    创建线程使用的底层函数和进程一样,都是clone。从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的。进程可以蜕变成线程。线程可看做寄存器和栈的集合。
    实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。
    因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。

    2.进程间通信方式

    进程间通信通常被称为:IPC(Internel-Process communication)
    (个人猜测:Internel内部是:相比于网络通信,IPC是内部的进程通信,走的是系统调用)
    主要有6种:

    image.png
    这6种方式:都不会走网络通信(TCP/IP那一套,直接走内核的内存等进行通信)
    网络socket(不同机器的不同进程)和命名socket的区别(同一台机器的不同进程)参考:
    https://blog.csdn.net/weixin_45121946/article/details/105045387
    (个人猜测:结合python实现猜测)
    • 命名Socket是最底层的实现方式
    • Pipe:基于memoryview+Socket实现
    • Queue:基于Pipe进一步封装实现等

    3.进程管理系统调用

    操作系统可以分为两种模式:

    • 内核态:操作系统内核使用的模式
    • 用户态:用户应用程序使用的模式

    系统调用(函数):是引起内核态和用户态切换的一种方式。
    与进程相关的主要的系统调用包括:
    1.fork
    fork用于创建一个与父进程相同的子进程,创建完进程后的子进程拥有和父进程一样的程序计数器、相同的CPU寄存器、相同的打开文件等
    2.exec
    exec 系统调用用于执行驻留在活动进程中的文件,调用 exec 后,新的可执行文件会替换先前的可执行文件并获得执行。也就是说,调用 exec 后,会将旧文件或程序替换为新文件或执行,然后执行文件或程序。新的执行程序被加载到相同的执行空间中,因此进程的 PID不会修改,因为我们没有创建新进程,只是替换旧进程。但是进程的数据、代码、堆栈都已经被修改。如果当前要被替换的进程包含多个线程,那么所有的线程将被终止,新的进程映像被加载执行。
    备注:
    进程映像(Process image)的概念
    进程映象是执行程序时:所需要的可执行文件(进程启动后,程序加载到内存,内存的分配的映像)。通常包括下面这些东西

    • 代码段(codesegment/textsegment):又称文本段,用俩存放指令,运行代码的一块内存空间。此空间大小在代码运行前就已经确定。内存空间一般属于只读,某些架构的代码也允许可写。代码段中:也有可能包含一些只读的常数变量,例如字符串常量等
    • 数据段(datasegment):可读可写,存储初始化全局变量和初始化的static变量,数据段中的数据的生命周期是随程序持续性(随进程持续性)。随进程持续性指的是:进程创建就存在,进程死亡就消失。
    • bss段(bss segement):可读可写。存储未初始化的全局变量和未初始化的static变量。bss段中的数据一般默认为0
    • 栈(stack):可读可写。存储的是函数或代码中国呢的局部变量(非static变量),栈的生存期随代码块持续性,代码块运行就给你分配空间,代码块结束,就自动回收空间。
    • 堆(heap):可读可写。存储的是程序运行期间动态分配的malloc/relloc的空间,堆的生存期随进程持续性,从malloc/relloc到free一直存在。


      image.png

      3.waitpid
      等待子进程结束或终止
      4.exit
      在许多计算机操作系统上,计算机进程的终止是通过执行exit系统调用命令执行的。

    二.python进程和线程

    1.基本概念

    1.进程间通信(IPC)

    进程是孤立的,但是可以彼此通信。进程间通信(IPC)通常有两种方式:
    1.基于消息传递
    一条消息:就是一块原始字节的缓存。
    基于消息的IPC通常有两种:

    • 管道
    • 队列

    2.共享内存(mmap模块):内存映射区域
    不太常见

    2.共享数据的同步和访问

    当多进程或者多线程需要共享数据时,就会出现数据同步和访问的问题。这也是并发编程常见的比较复杂的地方。

    3.并发编程与python

    python线程收到的限制比较多,主要原因是:python解释器内部使用了GIL(Global Interpreter Lock, 全局解释器锁)
    GIL:使得在任意时刻只允许单个python线程执行,无论系统上存在多少个可用的CPU核。
    GIL说明:
    Python解释器别一个锁保护,只允许一次执行一个线程,即使存在多核。

    • 在计算密集型程序中:这严重限制了多线程的作用。事实上,在计算密集型程序中使用线程,经常比仅仅按照顺序执行同样的工作慢的多。通常用multiprocessing等模块替代。
    • 在I/O密集型程序中:可能比较适合。比如:网络服务器中使用线程。

    2.multiprocessing

    1.进程process类

    用于创建和启动一个进程
    使用subprocess中的Popen类进行实现。
    底层还是调用os相关的接口,去创建进程等

    1)创建子进程时:会对当前一份进程镜像的拷贝。所以:传递给子进程的函数的参数等,都会在子进程中有一份一模一样的拷贝。子进程中对参数等对象的修改,完全不会影响到主进程。
    2)通过多进程通信(IPC)发送消息的方式:队列/管道中放入的项,在子进程中也是一个新的拷贝,修改其,不会影响到主进程中的该项。

    2.进程间通信

    1.Pipe类
    单向/双向都支持
    Pipe类使用:Connection类实现,Connection内部使用:memory+命名socket通信实现。
    管道内部使用:pickle模块作为序列化
    关于IPC通信命名socket(同一台机器不同进程)和网络通信socket的区别(不同机器之间的网络通信)详见:
    https://blog.csdn.net/weixin_45121946/article/details/105045387
    2.Queue类
    单向:更高级封装
    创建共享的进程队列。底层队列使用:管道和锁实现。另外,还需要运行支持线程以便将队列中的数据传输到底层管道中。
    3.共享数据与同步(一般不建议使用)
    其内部是基于mmap模块实现。

    3.threading

    由于GIL的存在,python的多线程可能更适用于IO密集型任务,而不太适合计算密集型任务。

    1.线程Thread类

    用于创建和启动一个现成
    (个人猜测)
    底层是通过:gevent(select、poll、epoll)等方式创建的线程。
    线程使用有两种方式:

    • 创建Thread对象,传递可调用对象等
    • 继承Thread类,重写run方法。之后新的类也是线程类,创建对象,执行方法等
    2.Timer类

    Timer类继承Thread类,支持在给定时间后开始执行线程。

    4.线程同步相关

    并发编程(主要是多线程,当然多进程也要考虑,有其他方式解决)涉及到共享数据,就会有数据的同步的问题。为了解决同步,通常是加锁方式。

    1.Lock对象(原语锁,是最底层的锁)

    原语锁(或互斥锁):是一个同步原语
    有两个状态:

    • 已锁定
    • 未锁定

    方法:

    • Lock():创建新的Lock对象,初始状态为未锁定
    • lock.acquire([blocking]):获取锁,如果有必要,需要阻塞到锁释放为止。如果设置blocking=False,当无法获取锁时,将立即返回False,如果成功获取锁则返回为True。
    • lock.release():释放一个锁。当锁处于未锁定状态时,或者从原本调用acquire()方法的线程不同的线程调用此方法,将会出现错误。(即:只能由获取到锁的线程,进行锁的释放)

    备注:
    如果有多个线程等待锁,当锁被释放时,只有一个线程能获得到它。等待县城获得锁的顺序没有定义。

    2.Rlock对象

    可重入锁(reentrant lock):是一个同步原语
    它允许拥有锁的线程执行嵌套的acquire()和release操作。在这种情况下,只有最外面的release()操作,才能将锁置为未锁定状态

    3.信号量与有边界的信号量(用的比较少)

    信号量是一个基于计数器的同步原语。可以通过设置value值,指定内部有多少信号量,可以用于线程的获取和释放。

    4.Condition(条件变量,对原语锁进行封装)

    1.condition用法介绍
    条件变量是构建在锁上的同步原语,当需要线程关注特定的状态变化或事件的发生时将使用这个锁。
    方法:

    • Condition([lock]):创建新的条件变量。lock是可选的Lock或Rlock实例。如果未提供lock参数,就会创建新的Rlock实例供条件变量使用。
    • cv.acquire(args):获取底层锁。此方法将调用底层锁上对应的acquire(args)方法
    • cv.release():释放底层锁。此方法将调用底层锁上对应的release()方法
    • cv.wait([timeout]):一直等待直到被唤醒,或者出现超时状态。
      此方法在调用线程已经获取锁之后调用。调用后:将释放底层锁,而且线程将进入睡眠状态,知道另一个线程在该条件变量上执行notify()或者notify_all()方法将其唤醒为止(通过内部锁实现)。在线程被唤醒后,线程将重新获取锁(重新获取底层锁,当然如果有多个线程在wait,同时被唤醒,这些线程都会去争取获得底层锁,但只有一个线程会获取到该底层锁,其他线程虽然被唤醒,但是会阻塞在获取外部锁的地方),方法也会返回。timeout是浮点数,单位为s。如果这单时间耗尽,线程将被唤醒,重新获取锁,而控制将被返回。
    • cv.notify([n]):唤醒一个或多个等待此条件变量的线程。此方法只会在带哦用线程已经获取锁之后调用。如果没有正在等待的线程,它就什么都不做。被唤醒的线程在他们重新获取底层锁之前不会从wait()返回
    • cv.notify_all():唤醒所有等待在此条件的线程。

    2.condition的实现原理(源码解析)
    http://timd.cn/python/threading/condition/
    http://darr-en1.top/2020/07/20/1/
    总结:
    condition实现主要依靠两层锁:

    • condition初始化时创建一把锁(外部锁,或者叫底层锁),使用时需要先对外部锁上锁;
    • 每次调用wait时,会先生成一个lock锁(内部锁),将内部锁放到算双端队列waiters中,
      然后上锁,再将外部锁释放。并再次获取内部锁block(备注:第二次再调用acquire会阻塞当前线程),等待其他线程调用notify释放该内部锁


      image.png

      备注:
      其中finally:是一定会走的流程。


      image.png
    5.event事件(最外层封装,对Condition做进一步封装,建议直接用Condition)

    event用于线程之间通信。其底层是依赖Condition+flag实现。
    一个线程发出"事件"信号,一个或多个其他线程等待线程信号。
    flag含义:

    • True:表示某个线程发出了信号,将flag设置为True。
    • False:表示当前的flag是False

    1.用法:

    • Event():创建新的Event实例,并将内部标志设为False。
    • e.is_set():只有当内部标志为True时才返回True
    • e.set():将内部标志设置为True。等待它变为True的所有线程都将被唤醒。(注意:虽然被唤醒,底层调用的是condition的wait方法,意味着:如果有多个线程等待,多个线程被唤醒后,会去竞争底层锁,只有一个线程能真正的获取到该锁,往下执行,其他线程依然阻塞在获取底层锁这里,等待下一次机会获取)
    • e.clear():将内部标志重制为False
    • e.wait([timeout]):线程阻塞在此event上,直到event玳标志为True。当然,如果进入时内部标志就为True,此方法将立即返回。否则,它将阻塞,直到另一个线程调用set()方法。

    5.concurrent包

    concurrent中目前只有一个模块:concurrent.futures
    该模块通过对多线程或者多进程的进一步封装,提供异步执行可调用对象的更高层接口。(更高的封装意味着易用性更好,灵活性更差)
    参考:
    https://docs.python.org/zh-cn/3/library/concurrent.futures.html
    贴上源码:

    image.png

    Future的实现原理简单分析(以ThreadPoolExecutor为例):
    1.submit函数中:

    • 创建并返回future对象。
      ProcessPoolExector的submit


      image.png

      ThreadPoolExecutor的submit


      image.png
      2.启动线程并提交执行任务
      image.png
      3.执行workItem的run方法
      image.png

      4.执行函数fn,并将结果设置到future中


      image.png

    5.Future的源码


    image.png
    image.png
    image.png

    6.总结

    1.多线程和多进程

    • 多进程:进程资源是相互隔离的,所以一般不会有共享数据的问题。主要关注是:进程之间通信的问题。
    • 多线程:多线程是在同一个进程内,共享同一个进程资源。不会有通信问题,主要关注是:对共享数据的同步问题。

    2.进程内通信(IPC)和网络通信

    • 进程内通信:主要是发送消息(对象序列化成字节的一块缓存),不走网络通信,其本质是:内存的偏移、拷贝等相关内存操作来完成。
    • 网络通信:需要走TCP/IP等网络,需要网卡支持。其也是将对象序列话成字节,然后通过网络发送、接受等。

    7.其他

    1.关于进程之间的参数都是拷贝,那么future在ProcessPoolExecutor中是如何实现异步的呢?

    参考:Pool的apply


    image.png

    相关文章

      网友评论

          本文标题:4.从linux到python:线程和进程

          本文链接:https://www.haomeiwen.com/subject/csuoartx.html