美文网首页
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:线程和进程

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

  • 浅谈linux线程模型和线程切换

    本文从linux中的进程、线程实现原理开始,扩展到linux线程模型,最后简单解释线程切换的成本。 刚开始学习,不...

  • 小猿圈操作系统之进程和线程区别

    进程与线程经常会放到一起提及,不管是学习linux、python还是其他语言,进程和线程都会用到,那么他们之间有什...

  • 进程管理

    进程管理进程和线程图形简单解释进程和线程管理VIM编辑Linux 系统资源信息的获取获取 Linux CPU 信息...

  • Python并发:多线程与多进程

    本篇概要 1.线程与多线程 2.进程与多进程 3.多线程并发下载图片 4.多进程并发提高数字运算 学习Python...

  • 开题

    linux 内核 线程和进程管理

  • Python ☞ day 15

    Python学习笔记之 进程和线程 多任务的原理 现代操作系统(Windows、Mac OS X、Linux、UN...

  • 20181204前端工程化Linux预备知识

    1. 操作系统概述 2. 远程登录Linux系统 3. 强调重要Linux命令 4. Linux进程与线程 5. ...

  • Android中进程和线程

    1,Linux中的进程管理1)进程和线程进程是资源分配的最小单位。线程是操作系统调度执行的最小单位。进程和线程是程...

  • 线程

    线程的概念 线程:light weight process,轻量级的进程,Linux环境下本质上仍是进程。和进程的...

网友评论

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

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