python多线程调⽤携程_进程,线程,协程与python的实现进程
进程
进程是程序执⾏的过程,包括了动态创建、调度和消亡的整个过程,进程是程序资源管理的最⼩单位。
进程管理的资源包括:CPU(寄存器),IO, 内存,⽹络资源等
进程地址空间进程通信方式
当创建⼀个进程时,操作系统会为该进程分配⼀个 4GB ⼤⼩的虚拟进程地址空间。
操作系统采⽤虚拟内存技术,把进程虚拟地址空间划分成⽤户空间和内核空间。每个进程的⽤户地址空间都是独⽴的,⼀般⽽⾔是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
⽤户空间按照访问属性⼀致的地址空间存放在⼀起的原则,划分成 5个不同的内存区域,总计 3G 的容量:
内存区域
存储内容
栈
局部变量,函数参数,函数的返回值
堆
动态分配的内存
BSS段
未初始化或初值为0的全局变量和静态局部变量
数据段
已初始化且初值⾮0的全局变量和静态局部变量
代码段
可执⾏⽂件的操作指令,可执⾏程序在内存中的镜像(只读)
在 x86 32 位系统⾥,Linux 内核地址空间是指虚拟地址从 0xC0000000 开始到 0xFFFFFFFF 为⽌的⾼端内存地址空间,总计 1G 的容量, 包括了内核镜像、物理页⾯表、驱动程序等运⾏在内核空间 。
进程间通信
当⼀个进程创建时可以共享程序代码的页,但它们各⾃有独⽴的数据拷贝(堆和栈),因此⼦进程对内存单元的修改对⽗进程是不可见的。
进程间的通信⽅式包括管道、消息队列、共享内存、信号量、信号和Socket。
管道
所谓的管道,就是内核⾥⾯的⼀串缓存。从管道的⼀端写⼊的数据,实际上是缓存在内核中的,另⼀端读取,也就是从内核中读取这段数据。另外,管道传输的数据是⽆格式的流且⼤⼩受限。匿名管道的通信的⽅式是单向的,如果要双向通信,需要创建两个管道。管道分为匿名管道和命名管道
匿名管道:没有名字标识,是只存在于内存的特殊⽂件,不存在于⽂件系统中。匿名管道只能⽤于存在⽗⼦关系的进程间通信。(创建的⼦进程会复制⽗进程的⽂件描述符,这样就做到了两个进程各有两个「 fd[0] 与 fd[1]」,两个进程就可以通过各⾃的 fd 写⼊和读取同⼀个管道⽂件实现跨进程通信了)它的⽣命周期随着进程创建⽽建⽴,随着进程终⽌⽽消失。shell 命令中的 “|”
命名管道:需要在⽂件系统创建⼀个类型为 p 的设备⽂件,不相⼲的进程从⽽可以通过这个设备⽂件进⾏通信。
管道这种通信⽅式效率低,不适合进程间频繁地交换数据
消息队列
消息队列是保存在内核中的消息链表,在发送数据时,会分成⼀个⼀个独⽴的数据单元,也就是消息体(数据块),消息体是⽤户⾃定义的数据类型,消息的发送⽅和接收⽅要约定好消息体的数据类型,所以每个消息体都是固定⼤⼩的存储块,不像管道是⽆格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
消息队列不适合⽐较⼤数据的传输,因为在内核中每个消息体都有⼀个最⼤长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。
消息队列通信过程中,存在⽤户态与内核态之间的数据拷贝开销。因为进程写⼊数据到内核中的消息队列时,会发⽣从⽤户态拷贝数据到内核态的过程,同理另⼀进程读取内核中的消息数据时,会发⽣从内核态拷贝数据到⽤户态的过程。
共享内存
现代操作系统,对于内存管理,采⽤的是虚拟内存技术,也就是每个进程都有⾃⼰独⽴的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存中。所以,即使进程 A 和 进程 B 的虚拟地址是⼀样的,
其实访问的是不同的物理内存地址,对于数据的增删查改互不影响。
共享内存的机制,就是拿出⼀块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写⼊的东西,另外⼀个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,⼤⼤提⾼了进程间通信的速度。
信号量
⽤了共享内存通信⽅式,带来新的问题,那就是如果多个进程同时修改同⼀个共享内存,很有可能就冲突了。为了防⽌多进程竞争共享资源,⽽造成的数据错乱,信号量使得共享的资源,在任意时刻只能被⼀个进程访问。
信号量是⼀个整型的计数器,表⽰的是资源个数,其值可以通过两个原⼦操作来控制,分别是 P 操作和 V 操作。主要⽤于实现进程间的互斥(初始化信号量为1)与同步(初始化信号量为0),⽽不是⽤于缓存进程间通信的数据。
信号
在 Linux 操作系统中, 为了响应各种各样的事件,提供了⼏⼗种信号,分别代表不同的意义。我们可以通过 kill -l 命令,查看所有的信号。
运⾏在 shell 终端的进程,我们可以通过键盘输⼊某些组合键的时候,给进程发送信号。例如:
Ctrl+C 产⽣ SIGINT 信号,表⽰终⽌该进程;
Ctrl+Z 产⽣ SIGTSTP 信号,表⽰停⽌该进程,但还未结束;
kill -9 1050 ,表⽰给 PID 为 1050 的进程发送 SIGKILL 信号,⽤来⽴即结束该进程
Socket
前⾯提到的管道、消息队列、共享内存、信号量和信号都是在同⼀台主机上进⾏进程间通信,那要想跨⽹络与不同主机上的进程之间通信,就需要 Socket 通信了。。
Python实现
multiprocessing
from multiprocessing import Process
import os, time
# ⼦进程要执⾏的代码
def run_proc(name):
time.sleep(2)
print('Run child process %s (%s)...' % (name, os.getpid()))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Process(target=run_proc, args=('test',))
print('Child process will start.')
p.start()
p.join()
print('Child process end.')
join所完成的⼯作就是阻塞当前进程,直到调⽤join⽅法的那个进程执⾏完,再继续执⾏当前进程multiprocessing.Pool 进程池
from multiprocessing import Pool
import os, time, random
def long_time_task(name):
print('Run task %s (%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(2)
end = time.time()
print('Task %s runs %0.2f seconds.' % (name, (end - start)))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Pool(2)
for i in range(3):
p.apply_async(long_time_task, args=(i,))
print('Waiting for all ')
p.close()
p.join()
print('All subprocesses done.')
Parent process 5584.
Waiting for all
Run task 0 (12836)...
Run task 1 (5916)...
Task 0 runs 2.00 seconds.
Run task 2 (12836)...
Task 1 runs 2.00 seconds.
Task 2 runs 2.00 seconds.
All subprocesses done.
Pool可以提供指定数量的进程供⽤户调⽤,当有新的请求提交到pool中时,如果池还没有满,那么就会创建⼀个新的进程⽤来执⾏该请求;但如果池中的进程数已经达到规定最⼤值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程。
进程间通信
基于共享内存:进程之间默认是不能共享全局变量的(⼦进程不能改变主进程中全局变量的值)。如果要共享全局变量需要⽤(multiprocessing.Value("d",10.0),数值)(multiprocessing.Array("i",[1,2,3,4,5]),数组)(multiprocessing.Manager().dict(),字典) (multiprocessing.Manager().list(),列表)。
基于管道:包括 multiprocessing.Pipe(),multiprocessing.Queue()。
from multiprocessing import Process, Queue
import os, time
# 写数据进程执⾏的代码:
def write(q):
print('Process to write: %s' % os.getpid())
for value in ['A', 'B', 'C']:
print('Put %s to queue.' % value)
q.put(value)
time.sleep(2)
# 读数据进程执⾏的代码:
def read(q):
print('Process to read: %s' % os.getpid())
while True:
value = q.get(True)
print('Get %s from queue.' % value)
if __name__=='__main__':
# ⽗进程创建Queue,并传给各个⼦进程:
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 启动⼦进程pw,写⼊:
pw.start()
# 启动⼦进程pr,读取:
pr.start()
# 等待pw结束:
pw.join()
# pr进程⾥是死循环,⽆法等待其结束,只能强⾏终⽌:
Process to read: 9504
Process to write: 10672
Put A to queue.
Get A from queue.
Put B to queue.
Get B from queue.
Put C to queue.
Get C from queue.
⼀个⼦进程向Queue中写数据,另外⼀个进程从Queue中取数据,当⼀个Queue为空的⽤get取数据会进程会被阻塞。
线程
线程
线程是操作操作系统能够进⾏运算调度的最⼩单位。线程被包含在进程之中,是进程中的实际运作单位,⼀个进程内可以包含多个线程,线程是资源调度的最⼩单位。
线程的内存模型
每个线程独⽴的线程上下⽂:⼀个唯⼀的整数线程ID,栈和栈指针,程序计数器,通⽤⽬的寄存器和条件码。
和其他线程共享的进程上下⽂的剩余部分:整个⽤户虚拟地址空间,包括只读代码段,读/写数据段,堆以及所有的共享库代码和数据区域,也共享所有打开⽂件的集合。
线程的状态
线程的状态分为:
可运⾏ (runnable):线程被创建之后,调⽤Start()函数就到了这个状态。
运⾏ (running):start()函数之后,CPU切换到了这个线程开始执⾏⾥⾯的Run⽅法就称为运⾏状态。
阻塞 (blocked):阻塞状态是指线程因为某种原因放弃了cpu执⾏权,暂时停⽌运⾏。直到线程进⼊可运⾏(runnable)状态,才有机会再次获得cpu 执⾏权 转到运⾏(running)状态。
根据程序分别处于 Running 以及 IO Blocked 两种状态的时间占⽐,可分为两种:
计算密集型 ,程序执⾏时⼤部分时间处于 Running 状态;
IO密集型 ,程序执⾏时⼤部分时间处于 IO Blocked 状态;
线程的调度
线程分类:
内核级线程:
内核线程建⽴和销毁都是由操作系统负责、通过系统调⽤完成,内核维护进程及线程的上下⽂信息以及线程切换。
程序⼀般不会直接使⽤内核线程,⽽是使⽤内核线程的⼀种⾼级接⼝-轻量级进程(Light-weight Process简称LWP ,是⼀种由内核⽀持的⽤户线程,每⼀个轻量级进程都与⼀个特定的内核线程关联。)。
优势:内核级线级能参与全局的多核处理器资源分配,充分利⽤多核 CPU 优势。
局限性:需要系统调度,系统调度代价⼤,需要在⽤户态和内核态来回切换;内核进程数量有限。
⽤户级线程
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论