进程、线程、 协程

进程

正在进行的一个过程或者说一个任务。而负责执行任务则是cpu。

进程和程序的区别

程序由一堆代码组成,进程是程序运行的过程

串行、并发、并行、阻塞、非阻塞

  • 串行:逐个运行程序,当前一个程序运行结束后才会运行下一个程序
  • 并发:伪并行,由单个cpu加多道技术实现,看起来想同时运行
  • 并行:同时运行,需要有多个cpu
  • 阻塞:程序运行中与到io阻塞会进入阻塞状态,cpu资源会切到其他程序
  • 非阻塞:没有io阻塞就是非阻塞

进程的状态

运行一个程序会进入就绪状态,当分配到cpu资源后进入运行状态,当遇到io时进入阻塞状态。当阻塞状态结束后会重新进入就绪状态等待分配cpu资源
在这里插入图片描述

创建进程

创建一个进程会在内存中开启一个进程空间,将主进程的资源复制一份,进程之间空间是隔离的,数据不共享。window系统创建进程必须放在main中

方式一

通过实例化进程对象开启进程

from multiprocessing import Process
def a(name):
    print(name)
if __name__ == '__main__':
    p = Process(target=a,args=("张三",))
    p.start() # 开启进程

实例化进程对象时传递的参数必须是元组类型,使用strat()方法开启进程

方式二

通过继承开启进程

from multiprocessing import Process
class A(Process):
    def __init__(self,name):
        super().__init__() # 继承父类init
        self.name = name
    def run(self): # 开启进程自动执行run方法
        print(self.name)
if __name__ == '__main__':
    a = A("张三")
    a.start() # 开启进程

进程的pid

每个进程都有一个唯一的pid,在cmd中可以通过pid tasklist获取进程的pid。在python中获取进程的pid需要导入os模块

from multiprocessing import Process
import os
def a():
    print(os.getpid()) # 获取当前线程pid
    print(os.getppid()) # 获取父线程的pid
if __name__ == '__main__':
    p = Process(target=a)
    p.start()

join

join是一种阻塞,主进程要等待设置join的子进程执行完毕后再执行

不设置join阻塞

代码:

from multiprocessing import Process
import time
def A(num):
    print("子",num)
    time.sleep(num)
if __name__ == '__main__':
    a = Process(target=A,args=(1,))
    ti = time.time()
    a.start()
    print("主",time.time()-ti)

结果:

0.0190494060516357421

设置join阻塞

代码:

from multiprocessing import Process
import time
def A(num):
    print("子",num)
    time.sleep(num)
if __name__ == '__main__':
    a = Process(target=A,args=(1,))
    ti = time.time()
    a.start()
    a.join()  # 给进程a设置join
    print("主",time.time()-ti)

结果:

11.4130237102508545

进程的其他属性

name:获取进程名
is_alive():获取进程的状态(True/Flase)
terminate():杀死进程

代码:

from multiprocessing import Process
import time
def a():
    pass
if __name__ == '__main__':
    p = Process(target=a)
    p2 = Process(target=a)
    p.start()
    p2.start() 
    print(p.name) # Process-1
    print(p2.name) # Process-2
    print(Process.is_alive(p))# 获取进程状态
    Process.terminate(p) # 杀死进程
    time.sleep(1)
    print(Process.is_alive(p))

结果:

Process-1
Process-2
True
False

daemon

守护进程,设置为守护进程的子进程,会在主进程结束后马上结束。设置守护进程需要在开启进程前设置!

代码:

from multiprocessing import Process
import time
def a():
    time.sleep(1)
    print("子")
if __name__ == '__main__':
    p = Process(target=a)
    p.daemon = True # 设置线程p为守护线程
    p.start()
    print("主")

结果:

僵尸进程和孤儿进程

僵尸进程

在开启进程后,父进程监视子进程运行状态,当子进程运行结束后一段时间内,将子进程回收,由于父进程和子进程是异步关系,不能在子进程结束后马上捕捉子进程的状态,如果子进程立即释放内存,父进程就无法再检测子进程的状态了,unix提供了一种机制,子进程在结束后会释放大部分内存,但会保留进程号、结束时间、运行状态,供主进程检测和回收

子进程在结束后被父进程回收前的状态就是僵尸进程。如果父进程由于某些原因一直未对子进程回收,这时子进程就会一直占用内存和进程号。这种情况就可以将父进程杀死,使子进程成为孤儿进程,等待init进行回收

孤儿进程

如果父进程已经结束了,但是子进程还在运行,这时子进程就是孤儿进程,孤儿进程由init回收

进程池

在程序实际处理问题过程中,如果任务量非常多时,开启成千上万个进程就非常不合理了。创建进程和销毁进程都需要消耗时间,而且操作系统也无法让如此多的进程同时执行,维护一个很大的进程列表的同时,调度的时候,还需要进行切换并且记录每个进程的执行节点,反而会影响程序的效率。所以产生了进程池。

定义一个进程池,在里面创建等量的进程。当需要使用进程时就从进程池中拿一个进程来处理任务,处理完毕后将进程重新放入进程池继续等待任务。如果需要执行的任务多于进程池内进程时,后面的任务就需要等待之前进程执行的任务执行完毕后,拿到空闲进程才能继续执行。这样做降低了操作系统的调度难度,节省了开关进程的时间,一定程度上实现并发效果

from concurrent.futures import ProcessPoolExecutor
import time
import random
def a(num):
    print(num)
    time.sleep(random.randint(1,4))
if __name__ == '__main__':
    p = ProcessPoolExecutor(5) # 指定最大进程数,默认为cpu核心数
    for i in range(20):
        p.submit(a,1)

线程

进程开启后由线程执行内部代码,进程只是用来把资源集中到一起(进程只是一个资源单位),线程才是cpu上的执行单位。

对比线程和进程

  • 开启进程内存开销非常大,对比开启线程速度很慢
  • 开启线程内存开销很小,开启速度很快,线程之间可以共享数据
  • 进程间需要借助队列等方式通信
  • 同一进程下的多个线程的pid相同

开启线程

开启线程和开启进程方式类似,但是开启线程可以不在main中。

方式一

通过实例化线程对象开启线程

from threading import Thread
def a(name):
    print(name)
if __name__ == '__main__':# 线程可以不必在main中开启
    t = Thread(target=a,args=("张三",)) 
    t.start() # 开启线程

方式二

通过继承开启线程

from threading import Thread
class A(Thread):
    def __init__(self,name):
        super().__init__()
        self.name = name
    def run(self):
        print(self.name)
a = A("张三")
a.start()

线程其他属性

setName():设置线程名
getName():获取线程名
is_alive():获取线程状态

代码:

from threading import Thread
def a(name):
    print(name)
if __name__ == '__main__':
    t = Thread(target=a,args=("张三",)) 
    t.start() # 开启线程
    t.setName("线程一")  # 设置线程名
    print(t.getName()) # 获取线程名
    print(t.is_alive()) # 获取线程状态

结果:

张三
线程一
False

守护线程

守护线程类似守护进程,但是守护线程要等待其他非守护线程和主线程结束后才结束,如果守护进程的生命周期小于其他线程则先结束

运行完毕并非终止运行

  1. 对主进程来说,运行完毕指的是主进程代码运行完毕

  2. 对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕

from threading import Thread
import time
def a(num):
    time.sleep(num)
    print("子线程")
if __name__ == '__main__':
    t = Thread(target=a,args=(1,))
    t.daemon = True
    t.start()
    print("主")

结果:

线程池

多线程指的是,在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。详细的讲分为4点:

  1. 多线程共享一个进程的地址空间
  2. 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
  3. 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。
  4. 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)
from concurrent.futures import ThreadPoolExecutor
import time
import random
def a(num):
    print(num)
    time.sleep(random.randint(1,4))
if __name__ == '__main__':
    t = ThreadPoolExecutor(5) # 设置最大线程数,默认为cpu核心*5
    for i in range(20):
        t.submit(a,1)

Event(事件)

线程的一个关键特性是每个线程都是独立运行,而且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。Event对象则可以解决这个问题。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。初始时,Event对象中的信号标志为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会一直阻塞,直到这个标志为真时。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

方法:

  1. isSet():返回event的状态值
  2. wait():当event的状态为Flase时将阻塞线程
  3. set():设置event的状态为True,所有阻塞池的线程激活进入就绪状态
  4. clear():将event的状态恢复为Flase

代码:

from threading import Thread
from threading import Event
import time
event = Event()
def check():
    print("检测服务器状态")
    time.sleep(3)
    print(event.is_set())
    event.set()
    print(event.is_set())
    print("服务器开启")
def conn():
    print("等待连接服务器")
    event.wait(3) # 三秒后如果还未set任然执行下面代码
    print("连接成功")
t1 = Thread(target=check)
t2 = Thread(target=conn)
t1.start()
t2.start()

结果:

检测服务器状态
等待连接服务器
False
True
连接成功
服务器开启

threading.local

为每个线程开辟单独的空间

import time
import threading
val1 = threading.local()
def task(i):
    val1.num = i
    time.sleep(1)
    print(val1.num)

for i in range(4):
    t = threading.Thread(target=task,args=(i,))
    t.start()

协程

是单线程下的并发,又称微线程,纤程。协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。

python的线程属于内核级别的,由操作系统控制调度(单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)。单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(非io操作的切换与效率无关)

对比线程和协程

协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级。在单线程内就可以实现并发的效果,最大限度地利用cpu。协程的本质是单线程下,无法利用多核。
一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程

协程的特点

  • 必须在只有一个单线程里实现并发
  • 修改共享数据不需加锁
  • 用户程序里自己保存多个控制流的上下文栈

Greenlet

使用greenlet模块可以非常简单的在多个任务直接切换

安装

pip3 install greenlet

演示

from greenlet import greenlet
import time
def a():
    print("A1") # 1
    g2.switch() # 切换到函数b
    time.sleep(2)
    print("A2") # 3
    g2.switch()

def b():
    print("B1") # 2
    g1.switch() # 切换到函数a
    print("B2") # 4
g1 = greenlet(a)
g2 = greenlet(b)
g1.switch() # 第一次进行switch时可以传入参数,之后就不需要了

结果:

A1
B1
A2
B2

greenlet只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时如果遇到io,那就原地阻塞,没有解决遇到IO自动切换来提升效率的问题。

Gevent

Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

安装

pip3 install gevent

示例

import gevent
import time
from gevent import monkey

monkey.patch_all() # 打补丁,将下面所有的任务阻塞都打上标记,必须写在最上面
def a(name):
    print(name + "A1")
    time.sleep(2) # 阻塞会被monkey识别
    print(name + "A2")
def b(name):
    print(name + "B1")
    time.sleep(1) 
    print(name + "B2")
g1 = gevent.spawn(a, "这是")
g2 = gevent.spawn(b, "这是")
# g1.join()
# g2.join()
gevent.joinall([g1,g2]) 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值