4.从linux到python:线程和进程
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种:
这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
贴上源码:
-
ThreadPoolExecutor
是Executor
的子类,它使用线程池来异步执行调用。 -
ProcessPoolExecutor
类是Executor
的子类,它使用进程池来异步地执行调用。ProcessPoolExecutor
会使用multiprocessing
模块,这允许它绕过 全局解释器锁 但也意味着只可以处理和返回可封存的对象。
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