python 十二 多进程、多线程、subprocess模块、threading模块

一、基础知识
参考文章:https://www.cnblogs.com/lincappu/category/1140217.html
1.1 线程、多线程
线程是一个基本的 CPU 执行单元。它必须依托于进程存活。一个线程是一个execution context(执行上下文),即一个 CPU 执行时所需要的一串指令。
多线程共享同个地址空间、打开的文件以及其他资源。
线程的类型:主线程、子线程、守护线程(后台线程)、前台线程
1.1.1 Python 多线程
在 Python 中,若使用CPython作为Python 解释器的话(用C语言实现的 Python 解释器,目前使用最广泛),无论CPU是单核还是多核,同时都只能由一个线程在执行。其根源是 GIL 的存在。GIL 的全称是 Global Interpreter Lock(全局解释器锁),某个线程想要执行,必须先拿到 GIL,我们可以把 GIL 看作是“通行证”,并且在一个 Python 进程中,GIL 只有一个。
拿不到通行证的线程,就不允许进入 CPU 执行。每次释放 GIL锁,线程进行锁竞争、切换线程,会消耗资源。并且由于 GIL 锁存在,Python 里一个进程永远只能同时执行一个线程(拿到 GIL 的线程才能执行),这就是为什么在多核CPU上,Python 的多线程效率并不高的根本原因。
如果希望应用程序更好地利用多核机器的计算资源,建议使用multiprocessing或concurrent.futures.ProcessPoolExecutor。然而,如果你想并发地运行多个I/O密集的任务,threading仍然是一个合适的模型。
注意
CPU 密集型:程序比较偏重于计算,需要经常使用 CPU 来运算。多线程不适用
I/O 密集型:顾名思义就是程序需要频繁进行输入输出操作。 适用多线程,爬虫程序就是典型的 I/O 密集型程序。
windows 系统不支持fork,只支持多线程

1) 创建多线程
Python提供两个模块进行多线程的操作,分别是thread和threading,前者是比较低级的模块,用于更底层的操作,提供了基本的线程和锁的支持,threading提供更高级别、功能更强的线程管理功能。
——方法1:直接使用threading.Thread()
——方法2:继承threading.Thread来自定义线程类,重写run方法


----> Thread 对象使用start()方法开始线程的执行,使用join() 方法挂起程序,直到线程结束。
多数的线程服务器有同样的结构;
主线程是负责侦听请求的线程;
主线程收到一个请求的时候,新的工作线程会被建立起来,处理客户端请求;
客户端断开时,工作线程将终止;
线程划分为用户线程和后台(daemon)进程,setDaemon将线程设置为后台进程。
–》threading 模块定义了以下函数︰
threading.active_count() 返回当前处于alive状态的Thread对象的个数。返回的数目等于enumerate()返回的列表的长度。
threading.current_thread() 返回当前的Thread对象,对应于调用者控制的线程。如果调用者控制的线程不是通过threading模块创建的,则返回一个只有有限功能的虚假线程对象。
threading.enumerate() 返回当前活着的Thread对象的列表。该列表包括守护线程、由current_thread()创建的虚假线程对象和主线程。它不包括已终止的线程和尚未开始的线程。
threading.main_thread() 返回主 Thread 对象。在正常情况下,主线程是从 Python 解释器中启动的线程。


线程本地数据 : 本地数据是指值特定于具体线程的数据。要管理线程本地数据,只需创建 local(或其子类)的一个实例并在它上面存储属性:
mydata = threading.local()
mydata.x = 1
class threading.local 表示线程本地数据的一个类。


Thread 对象
Thread类表示在单独的控制线程中运行的活动。
class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None
参数:group应为None;保留用于在实现ThreadGroup类时的未来扩展。
target是将被run()方法调用的可调用对象。默认为None,表示不调用任何东西。
name是线程的名字。默认情况下,以“Thread-N”的形式构造一个唯一的名字,N是一个小的十进制整数。
args是给调用目标的参数元组。默认为()。
kwargs是给调用目标的关键字参数的一个字典。默认为{}
daemon 如果None(默认值),daemonic属性从当前线程继承。


2) 线程合并
Join函数执行顺序是逐个执行每个线程,执行完毕后继续往下执行。主线程结束后,子线程还在运行,join函数使得主线程等到子线程结束时才退出。
3)线程同步与互斥锁
线程之间数据共享的。当多个线程对某一个共享数据进行操作时,就需要考虑到线程安全问题。threading模块中定义了Lock 类,提供了互斥锁的功能来保证多线程情况下数据的正确性。
#创建锁
mutex = threading.Lock()
#锁定
mutex.acquire([timeout])
#释放
mutex.release()

  1. 可重入锁(递归锁)
    为了满足在同一线程中多次请求同一资源的需求,Python 提供了可重入锁(RLock)。RLock内部维护着一个Lock和一个counter变量,counter 记录了 acquire 的次数,从而使得资源可以被多次 require。直到一个线程所有的 acquire 都被 release,其他的线程才能获得资源。

  2. 守护线程
    如果希望主线程执行完毕之后,不管子线程是否执行完毕都随着主线程一起结束。我们可以使用setDaemon(bool)函数,它跟join函数是相反的。它的作用是设置子线程是否随主线程一起结束,必须在start() 之前调用,默认为False。
    守护进程参数:
    start() 开始线程的活动。 每个线程对象必须只能调用它一次。它为对象的run()方法在一个单独的控制线程中调用做准备。在相同的线程对象上调用该方法多次将引发一个RuntimeError。
    run() 表示线程活动的方法。可以在子类中覆盖这个方法。标准的run()方法调用传递给对象构造函数target参数的可调用对象,如果存在,分别从args和kwargs参数获取顺序参数和关键字参数。
    join(timeout=None) 等待直至线程终止。当timeout参数存在且不为None时,它应该以一个浮点数指定该操作的超时时间,单位为秒(可以是小数)。由于join()总是返回None,必须在调用is_alive()之后来join()决定是否发生超时 - 如果线程仍然存在,则join()调用超时。如果timeout参数不存在或者为None,那么该操作将阻塞直至线程终止。一个线程可以被join()多次。如果尝试join当前的线程,join()会引发一个RuntimeError,因为这将导致一个死锁。
    ident 线程的ID,如果线程还未启动则为None。它是一个非零的整数。
    is_alive() 返回线程是否还活着。在run()方法刚开始之前至run()方法刚终止之后,该方法返回True。模块级别的函数enumerate()返回所有活着的函数的一个列表。
    daemon 布尔值,该值指示是否此线程一个守护进程线程 (True),或不 (False)。它必须在调用 start() 之前设置,否则引发 RuntimeError。从创建的线程继承其初始的值,主线程不是守护线程,因此在主线程创建的所有线程的默认 daemon = False。


  1. 定时器
    如果需要规定函数在多少秒后执行某个操作,需要用到Timer类。Timer是Thread的子类,Timers通过调用它们的start()方法作为线程启动。timer可以通过调用cancel()方法(在它的动作开始之前)停止。
    class threading.Timer(interval, function, args=None, kwargs=None)
    创建一个timer,在interval秒过去之后,它将以参数args和关键字参数kwargs运行function 。如果args为None(默认值),则将使用空列表。如果kwargs为None(默认值),则将使用空的字典。
    cancel() 停止timer,并取消timer动作的执行。这只在timer仍然处于等待阶段时才工作。

1.2 进程、多进程
进程是指一个程序在给定数据集合上的一次执行过程,是系统进行资源分配和运行调用的独立单位,即为操作系统中正在执行的程序。每个应用程序都有一个自己的进程,每一个进程启动时都会最先产生一个线程,即主线程。然后主线程会再创建其他的子线程。
多进程共享物理内存、磁盘、打印机以及其他资源。

  1. 创建多进程
    Python 要进行多进程操作,需要用到muiltprocessing库,其中的Process类跟threading模块的Thread类很相似。
    方法1:直接使用Process
    方法2:继承Process来自定义进程类,重写run方法

  2. 多进程通信
    进程之间不共享数据的。如果进程之间需要进行通信,则要用到Queue模块或者Pipi模块来实现。
    ——Queue
    Queue 是多进程安全的队列,可以实现多进程之间的数据传递。它主要有两个函数,put和get
    put() 用以插入数据到队列中,put 还有两个可选参数:blocked 和 timeout。如果 blocked 为 True(默认值),并且 timeout 为正值,该方法会阻塞 timeout 指定的时间,直到该队列有剩余的空间。如果超时,会抛出 Queue.Full 异常。如果 blocked 为 False,但该 Queue 已满,会立即抛出 Queue.Full 异常。
    get()可以从队列读取并且删除一个元素。同样,get 有两个可选参数:blocked 和 timeout。如果 blocked 为 True(默认值),并且 timeout 为正值,那么在等待时间内没有取到任何元素,会抛出 Queue.Empty 异常。如果blocked 为 False,有两种情况存在,如果 Queue 有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出 Queue.Empty 异常。

3) 进程池
创建多个进程,可以使用Pool模块。
Pool 常用的方法如下:
apply() 同步执行(串行)
apply_async() 异步执行(并行)
terminate() 立刻关闭进程池
join() 主进程等待所有子进程执行完毕。必须在close或terminate()之后使用
close() 等待所有进程结束后,才关闭进程池


二 、 fork、wait
2.1 fork(分岔)
当某一命令执行时,父进程(当前进程)fork 出一个子进程,父进程将自身资源拷贝一份,命令在子进程中运行时,就具有和父进程完全一样的运行环境。
os.fork() #生成子进程,后续代码同时在父子进程中执行
pid= os.fork() #返回值是个数字,对于父进程,返回值是进程PID,子进程运行返回值是0 (即0或非0
# watch -n1 ps a #查看进程正在运行的情况,当子进程变为僵尸进程时,显示为Z,kill 无法杀死僵尸进程,可以通过杀死父进程来结束僵尸进程。

2.2 wait
父进程通过os.wait() 来得到子进程是否终止的信息,在子进程终止和父进程调用wait()之间的这段时间,子进程被称为zombie(僵尸进程)。如果子进程还没有终止,父进程先退出了,那么子进程会持续工作。系统自动将子进程的父进程设置为init进程,init将来负责清理僵尸进程。
使用轮询解决zombie问题。
os.waitpid(pid, options) :一次只能结束一个僵尸进程
python可以使用waitpid()来处理子进程。waitpid() 接受 两个参数,第一个参数设置为-1,表示与wait()函数相同;第二个参数如果设置为0表示挂起父进程,直到程序退出,设置为1表示不挂起父进程
waitpid() 的返回值:如果子进程尚未结束则返回0,否则返回子进程的PID(即 无僵尸进程需要处理返回0,有僵尸进程需要处理,返回子进程PID)。


三、 subprocess模块——子进程管理。
在Python中,我们通过标准库中的subprocess包来fork一个子进程,并运行一个外部的程序。subprocess模块允许你生成新进程,连接到其输入/输出/错误管道,并获取其返回码.
subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, shell=False, timeout=None, check=False) 运行args描述的命令。等待命令完成,然后返回一个CompletedProcess实例。

3.1 subprocess.call() (替代os.system())
父进程等待子进程完成,返回退出信息(returncode,相当于Linux exit code)

3.2 subprocess.check_call()
父进程等待子进程完成,返回0。

检查退出信息,如果returncode不为0,则举出错误subprocess.CalledProcessError,该对象包含有returncode属性,可用try…except…来检查

3.3 subprocess.check_output()
父进程等待子进程完成,返回子进程向标准输出的输出结果。

检查退出信息,如果returncode不为0,则举出错误subprocess.CalledProcessError,该对象包含有returncode属性和output属性,output属性为标准输出的输出结果,可用try…except…来检查。

child.poll() # 检查子进程状态
child.kill() # 终止子进程
child.send_signal() # 向子进程发送信号
child.terminate() # 终止子进程
child.communicate():利用communicate()方法来使用PIPE给子进程输入
child.pid () 存储子进程的pid

3.4 subprocess.Popen():(替代os.popen())
参数:
args:shell命令,可以是字符串,或者序列类型,如list,tuple。
bufsize:缓冲区大小,可不用关心
stdin,stdout,stderr:分别表示程序的标准输入,标准输出及标准错误
shell:与上面方法中用法相同
cwd:用于设置子进程的当前目录
env:用于指定子进程的环境变量。如果env=None,则默认从父进程继承环境变量
universal_newlines:不同系统的的换行符不同,当该参数设定为true时,则表示使用\n作为换行符

3.5 子进程的文本流控制
child.stdin
child.stdout
child.stderr
可以在Popen()建立子进程的时候改变标准输入、标准输出和标准错误,并可以利用subprocess.PIPE将多个子进程的输入和输出连接在一起,构成管道(pipe)。communicate()是Popen对象的一个方法,该方法会阻塞父进程,直到子进程完成。


3.6 案例
1)import subprocess
child1 = subprocess.Popen([“ls”,"-l"], stdout=subprocess.PIPE)
child2 = subprocess.Popen([“wc”], stdin=child1.stdout,stdout=subprocess.PIPE)
out = child2.communicate()
print(out)
subprocess.PIPE实际上为文本流提供一个缓存区。child1的stdout将文本输出到缓存区,随后child2的stdin从该PIPE中将文本读取走。child2的输出文本也被存放在PIPE中,直到communicate()方法从PIPE中读取出PIPE中的文本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值