多进程和多线程
进程的概念
进程:一个正在执行的程序
- 计算机程序是存储在磁盘上的可执行二进制(或其他类型)文件,只有把它们加载到内存中,并被操作系统调用,它们才会拥有其自己的生命周期。
- 进程是表示的一个正在执行的程序。
- 每个进程都拥有自己的地址空间、内存、数据栈以及其他用于跟踪执行的辅助数据。
- 操作系统负责其上所有进程的执行,并为这些进程合理地分配执行时间。
- 进程之间是独立的,不能共享彼此的数据。
并行与并发
并行:同时进行多个任务
并发:短时间内,运行多个任务
并发:单CPU,多进程并发
无论是并行还是并发,在用户看来都是'同时'运行的,不管是进程还是线程,都只是一个任务而已,真实干活的是cpu,cpu来做这些任务,而一个cpu同一时刻只能执行一个任务
一 并发:是伪并行,即看起来是同时运行。单个cpu+多道技术就可以实现并发,(并行也属于并发)
并行:多CPU(同时运行,只有具有多个cpu才能实现并行)
并行满足的条件是总进程数量不多于 CPU核心数量!因此,现在PC运行的程序大部分都是轮询调度产生的并行假象
举例:比如PC有四个核,六个任务,这样同一时间有四个任务被执行,假设分别被分配给了cpu1,cpu2,cpu3,cpu4,
一旦任务2遇到I/O就被迫中断执行,此时任务5就拿到cpu1的时间片去执行,这就是单核下的多道技术
而一旦任务2的I/O结束了,操作系统会重新调用它(需知进程的调度、分配给哪个cpu运行,由操作系统说了算),可能被分配给四个cpu中的任意一个去执行
所以,现代计算机经常会在同一时间做很多件事,一个用户的PC(无论是单cpu还是多cpu),都可以同时运行多个任务(一个任务可以理解为一个进程)。
多道技术:内存中同时存入多道(多个)程序,cpu从一个进程快速切换到另外一个,使每个进程各自运行几十或几百毫秒,这样,虽然在某一个瞬间,一个cpu只能执行一个任务,但在1秒内,cpu却可以运行多个进程,这就给人产生了并行的错觉,即伪并发,以此来区分多处理器操作系统的真正硬件并行(多个cpu共享同一个物理内存)
例子:在python中执行耗时操作
结果:
用进程来分担耗时任务后:
python进程使用流程
线程的概念
- 线程被称作轻量级进程。
- 与进程类似,不过它们是在同一个进程下执行的。
- 并且它们会共享相同的上下文。
- 当其他线程运行时,它可以被抢占(中断)和临时挂起(也称为睡眠)
- 线程的轮训调度机制类似于进程的轮询调度,只不过这个调度不是由操作系统来负责,而是由Python解释器来负责。
GIL锁
Python在设计的时候,还没有多核处理器的概念。因此,为了设计方便与线程安全,直接设计了一个锁。这个锁要求,任何进程中,一次只能有一个线程在执行。
因此,并不能为多个线程分配多个CPU。所以Python中的线程只能实现并发,而不能实现真正的并行。但是Python3中的GIL锁有一个很棒的设计,在遇到阻塞(不是耗时)的时候,会自动切换线程。
Scrapy、Django、Flask、Web2py ...等框架 都是使用多线程来完成
GIL锁带给我们的新认知,遇到阻塞就自动切换。因此我们可以利用这种机制来有效的避开阻塞 ,充分利用CPU
例子:使用线程来避开阻塞任务
python线程使用流程
计算密集型和IO密集型
IO密集型遇到等待时,不消耗CPU,CPU会调度其他程序执行,使用多线程可以有效的进行并发(爬虫的请求是IO密集型),线程在系统中所占资源较少。
计算密集型:CPU会一直计算,此时使用多线程,反而会耗时更多,此时使用多进程(CPU越多,性能越好)。
使用多进程/多线程实现并发服务器:
import socket from multiprocessing import Process from threading import Thread def read(conn,addr): while True: data = conn.recv(1024) if data: print('客户端{}的消息:{}'.format(addr,data.decode())) conn.send(data) else: conn.close() print('客户端{}断开连接'.format(addr)) break if __name__ == '__main__': server = socket.socket() server.bind(('', 9998)) # 注意这些代码要写在if...main...判断下面,因为在Windows中,使用进程的模式类似导入,在判断外面的代码会在子进程执行 server.listen(5) while True: print('等待客户端连接') conn,addr = server.accept() # 有客户端连接,就往下进行 print('客户端{}已连接'.format(addr)) # p = Process(target=read,args=(conn,addr)) # 使用多进程,每来一个客户端连接,就分出一个进程去和它一对一处理 # p.start() t = Thread(target=read,args=(conn,addr)) # 使用多线程,每来一个客户端连接,就分出一个线程去和它一对一处理 t.start()
补充
fork介绍
Unix/Linux操作系统提供了一个fork()
系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()
调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0
,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()
就可以拿到父进程的ID。
import os if __name__ == '__main__': print('进程 (%s) start...' % os.getpid()) pid = os.fork() # time.sleep(10) if pid == 0: # 若fork()返回0为子进程 print("子进程{},父进程{}".format(os.getpid(), os.getppid())) #getpid()获取当前进程的pid,getppid()获取父进程的pid else: # 父进程fork返回子进程的id print("父进程{},子进程{}".format(os.getpid(), pid)) >> 进程 (3130) start... 父进程3130,子进程3131 子进程3131,父进程3130
multiprocessing模块介绍
python中的多线程无法利用CPU资源,在python中大部分计算密集型任务使用多进程。如果想要充分地使用多核CPU的资源(os.cpu_count()查看)
python中提供了非常好的多进程包multiprocessing。
multiprocessing模块用来开启子进程,并在子进程中执行功能(函数),该模块与多线程模块threading的编程接口类似。
multiprocessing的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。
计算密集型任务:如金融分析,科学计算
multiprocessing.current_process() :在任何一个进程中搞清楚自己是谁,在线程中也有类似方法
Process类的介绍
1.创建进程的类
Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,表示一个子进程中的任务(尚未启动) 强调: 1. 需要使用关键字的方式来指定参数 2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号
2.参数介绍
group参数未使用,值始终为None target表示调用对象,即子进程要执行的任务 args表示调用对象的位置参数元组,args=(1,2,'al',) kwargs表示调用对象的字典,kwargs={'name':'al','age':18} name为子进程的名称
3.方法介绍
p.start():启动进程,并调用该子进程中的p.run()
p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法
p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。
如果p还保存了一个锁那么也将不会被释放,进而导致死锁,线程中无此方法
p.is_alive():如果p仍然运行,返回True
p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)再往下运行。timeout是可选的超时时间,
需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程
4.属性介绍
p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
p.name:进程的名称
p.pid:进程的pid,线程中是t.ident 注意只有进程或线程已经启动才会分配pid或ident
p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)
p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,
这类连接只有在具有相同的身份验证键时才能成功(了解即可)
以用面向对象的形式使用进程与线程
关键点:
- 继承Process或Thread类
- 重写__init__方法
- 重写run方法
from multiprocessing import Process import time import random import os
class pro(Process): #继承Process类 def __init__(self,name): super().__init__() # 重写__init__方法 这里是直接调用父类的初始化方法 self.name = name
def run(self): # 重写run方法 print('%s is working,父进程%s,当前进程%s'%(self.name,os.getppid(),os.getpid())) time.sleep(1) print('%s end'%self.name)
if __name__ =='__main__': p1 = pro('a') p2 = pro('b') p3 = pro('c') p1.start() p2.start() p3.start() print('主进程',os.getpid()) >> 主进程 9116 b is working,父进程9116,当前进程4316 a is working,父进程9116,当前进程6664 c is working,父进程9116,当前进程10132 b end a end c end
总结:
Windows用的导入(所以if...main...判断外面的代码,在子进程仍会执行) 方法: p = Process(target=func,name='p1',daemon=True) #实例的时候可以指定方法、名字、是否守护进程 p.current_process() 获取当前进程对象 p.is_alive() 判断进程实例是否在运行 是返回True 否返回False p.terminate() 结束进程 线程无此方法 p.name 获取进程名字 p.join() 等待子进程结束再往下执行 p.daemon = True 把进程设为守护进程 (父进程结束,守护进程也结束) 注意:如果设置了join() 那么terminate和daemon 就不管用啦 p.pid 获取当前进程的pid 线程是t.ident 注意:只有进程(或线程)启动之后,操作系统(或python解释器)才会分配pid(或ident) 默认在子进程当中,会关闭标准输入 import sys import os sys.stdin = os.fdopen(0) #加上这行代码,子进程才能使用标准输入 多线程的方法与属性 t.setDaemon(True) t.setName('p1') t.getName()