系列文章目录


文章目录

  • 系列文章目录
  • 一、进程(Process)
  • 二、创建多进程的两种方式
  • 三、进程对象的方法
  • 四、僵尸进程、孤儿进程和守护进程
  • 五、互斥锁(进程)
  • 六、进程间通信
  • 七、生产者、消费者模型

  • 八、线程(Thread)
  • 九、创建多线程的两种方式
  • 十、线程对象的方法
  • 十一、守护线程
  • 十二、互斥锁(线程)
  • 十三、全局解释器锁(GIL)
  • 十四、多进程与多线程比较


一、进程(Process)

  • 进程的概念:
    进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。 可以简单理解为程序的一次运行过程。
    当我们启动浏览器,就是启动了一个进程。
    进程是一种抽象的概念,从来没有统一的标准定义。
  • 进程状态图(五状态模型):

注意!阻塞是指阻塞态,非阻塞是指就绪态和运行态

二、创建多进程的两种方式

  • 第一种,直接实例化(使用的比较多):
from multiprocessing import Process


def foo(a):
    """需要多进程执行的函数"""
    pass

if __name__ == '__main__':
    # 创建进程对象,args用来向函数传递参数
    p = Process(target=foo, args=(1,))
    # 启动新进程
    p.start()

注意:在Windows系统中,进程的创建和启动需要写在main语句内部;在unix类系统中,则不需要。

  • 第二种,自定义进程类:
    必须继承Process类,且需要多进程执行的函数,名称必须为run。
from multiprocessing import Process


class MyProcess(Process):
    """自定义进程类"""
    def __init__(self, name):
        super(MyProcess,self).__init__()
        self.name = name

    def run(self):
        print(self.name)



if __name__ == '__main__':
    # 创建进程对象,直接传参给__init__方法
    p = MyProcess('hugh')
    p.start()

三、进程对象的方法

  • 阻塞当前进程——join()
    阻塞当前上下文环境的进程(也就是创建新进程的进程),直到调用此方法的进程终止或到达指定的timeout(可选参数):
if __name__ == '__main__':
    p = Process(target=foo, args=(1,))
    p.start()

    # 阻塞当前进程,直到p进程结束
    p.join()

    print('p进程结束,继续执行当前进程!')
  • 查看当前进程的pid——current_process().pid

pid是进程的唯一标识。

from multiprocessing import current_process

current_process().pid
# 返回:233

使用os模块会更方便:

import os

# 获取当前进程的pid
os.getpid()

# 获取父进程的pid
os.getppid()
  • 终止进程——p.terminate()
  • 判断进程是否存活——p.alive()

四、僵尸进程、孤儿进程和守护进程

  • 僵尸进程:
    当一个子进程终止后,操作系统并不会立刻释放该进程的pid、端口号等,而是会保留一段时间。以供其父进程查询该子进程的一些基本信息。
    进程进入上述阶段,就称之为僵尸进程。所有进程都会步入这一阶段。
  • 孤儿进程:
    父进程意外结束,但子进程存活,则该子进程称为孤儿进程。孤儿进程会被系统的init进程“收养”,父进程更改为init进程。
  • 守护进程:
    默认情况下,当父进程终止时,如果有子进程存活,父进程会等待子进程终止后再退出。
    而将一个子进程设置为守护进程后,就可以保证其父进程终止时,该子进程也立刻终止。
    设置方法:p.deamon = True,该属性默认值为False必须在p.start()方法之前进行设置,否则无效。

五、互斥锁(进程)

  • 临界资源:
    一次仅允许一个进程使用的资源称为临界资源(I/O设备、变量、缓冲区等)。
    临界资源必须保证每次只能由一个进程使用,否则会出现问题,比如:打印机不可能同时打印多个进程的结果,若将一个进程的结果打印几行,再打印另一个进程的结果,这会使打印的结果变得无法使用。
  • 互斥锁:
    本质上是一个计数器,而这个计数器的取值只有0或者1。
    0代表不能加锁,意味着不能去访问临界资源,互斥锁当中的加锁接口就会进行阻塞;
    1代表可以加锁,意味着可以去访问临界资源,互斥锁中的加锁接口会正常返回。
  • 使用方法:
from multiprocessing import Lock

# 实例化Lock对象
mutex = Lock()

# 加锁(谁抢到算谁的,随机的)
mutex.acquire()

"""
抢到锁的进程就能使用临界资源,没抢到的就会进入阻塞状态
使用完毕必须释放锁,否则其他进程将无法使用该临界资源
"""

# 释放锁
mutex.release()
  • 注意:
    不要轻易使用锁,大量使用锁容易产生死锁现象;
    只在临界资源附近使用锁,否则会影响性能;

六、进程间通信

  • IPC机制:
    IPC (Inter-Process Communication),意为进程间的通信。一个操作系统不同的进程,有自己的进程内存空间,其中的数据不共享,因此进程间的通信就需要采用一定的机制。
    传统的进程通信的方式有 Socket,管道,内存共享,消息队列等。而在python中,队列是最常用的通信机制。
  • 队列:
from queue import Queue

# 创建队列对象,参数为队列存可放对象的最大值
q = Queue(10)

# 存放
q.put('数据')

# 取出,遵循队列先进先出的原则
q.get()
  • 存放数据时,如果队列已满,该进程会阻塞;
  • 取出数据时,如果队列为空,该进程会阻塞;但可以设置一个等待时长参数:timeout=3,代表阻塞3秒,3秒之后会抛出异常。
  • 不想在取出时阻塞,可以使用q.get_nowait()方法,该方法会在队列为空时直接抛出异常。
  • 可以在存放之前,使用q.fill()判断队列是否已满;取出前,用q.empty()判读是否为空。

以上所有判断方法,在多进程中是不精准的!也许上一时刻,队列判断为空,下一时刻就被其他进程放入了数据。

七、生产者、消费者模型

生产者:比喻产生数据的进程;
消费者:比喻获取数据的进程。

  • 生产者和消费者之间速度不匹配的问题:
    当生产者生产速度很快,而消费者来不及消费的时候,生产者就会被阻塞,导致程序效率下降。
    引起该问题的原因很明显,就是生产者缺乏一个存放数据的容器。如果有了该容器,生产者就可以不停的生产数据,然后放入容器,而不用去管消费者消费速度的问题了。
  • 生产者、消费者模型:
    消费者、生产者模型,就是用来解决上述及类似问题的,它使用一个容器存放数据,对生产者和消费者进行了解耦,提高了程序的性能。
  • 使用JoinableQueue([maxsize])类实现:

使用queue实现的模型,在结束的时候不好处理,因为消费者不知道生产者什么时候停止生产数据,等数据取完就会进入阻塞状态,一直等待生产者产生新的数据。而JoinableQueue解决了这一问题。

  • JoinableQueue类是 Queue的子类,额外添加了task_done()join() 方法。
    当消费者取数据后,使用对列.task_done()方法用来将计数器减一。
    使用队列.join()方法,该方法会一直阻塞,直到计数器为零,说明队列为空。然后我们就可以放心的终止消费者进程。
    此处,我们通设置守护进程的方式终止消费者进程。
from multiprocessing import Process, JoinableQueue
import time
import random
import os
 
 
def consumer(q):
    """消费者"""
    while True:
        res = q.get()
        if res is None:  # 收到信号就结束
            break
        time.sleep(random.randint(2, 5))
        print('%s消耗了%s' % (os.getpid(), res))
        # 计数器减一
        q.task_done() 
 
 
def producer(name, q):
    """生产者"""
    for i in range(5):
        time.sleep(random.randint(1, 3))
        res = "%s %s" % (name, i)
        q.put(res)
        print('%s生产了%s' % (os.getpid(), res))
 
 
if __name__ == '__main__':
    q = JoinableQueue()
    p_one = Process(target=producer, args=("红茶", q))
    p_two = Process(target=producer, args=("橙汁", q))

 
    c_one = Process(target=consumer, args=(q,))
    c_two = Process(target=consumer, args=(q,))
 
    # 消费者设置为守护进程
    c_one.daemon = True
    c_two.daemon = True
 
    p_lst = [p_one, p_two, c_one, c_two]
 
    for p in p_lst:
        p.start()
    
    # 等待生产者生产完数据
    p_one.join()
    p_two.join()
    p_thr.join()
    
    # 等待计数器清零
    q.join()
 
    print("主进程结束")

八、线程(Thread)

  • 线程的概念:
    线程是指进程中的一个执行流程,一个进程中可以运行多个线程。进程是资源分配的基本单位,而线程是任务执行的基本单位。每个进程都至少有一个线程。
    启动一个浏览器是启动一个进程,而新建一个浏览器标签页就是新建了一个线程。
    在进程中新建线程,无需申请内存空间,因此,新建线程的开销要远小于新建进程

同一进程下,不同线程之间,数据是共享的!

九、创建多线程的两种方式

与多进程的创建方法是大同小异的。

  • 第一种(使用的比较多):
from threading import Thread
t = Thread(target=多线程任务, args=参数元组)
t.start()

注意:线程不同于进程,即便是在Windows系统中,创建和启动线程的代码不需要写在main语句内部

  • 第二种:
from threading import Thread

class MyThread(Thread):
    def __init__(self, name):
        super(MyThread, self).__init__()
        self.name = name
    
    # 方法名必须为run
    def run(self):
        print(self.name)
        
t = MyThread('hugh')
t.start()

十、线程对象的方法

  • 线程对象的join()方法和进程的一样。
  • 返回指定线程对象的线程名——t.getName()
  • 设置指定线程对象的线程名——t.setName()
  • 返回当前线程的ID——threading.current_thread().ident
  • 返回当前线程的名字——threading.current_thread().name
  • 统计当前活跃的线程数——activate_count(),其中除了通过代码创建的线程外,还包括主线程本身 。
  • 判断进程是否存活——t.Alive()
  • 返回所有活着的线程列表,不包括已经终止的线程和未开始的线程——threading.enumerate()

十一、守护线程

主线程结束之后不会立即终止,会等待同一进程下的所有非守护线程结束后才终止。

守护线程的设置方法,与进程一样:t.deamon = True

十二、互斥锁(线程)

除了模块不同外,其他和进程一模一样。

from threading import Lock

十三、全局解释器锁(GIL)

  • python解释器版本:
    python解释器有很多版本,它们是用其他语言实现的python解释器,比如:用java写的jpython、用c写的cpython、用python自己写的pypy;这些解释器,各有千秋,但最普遍的还是cpython。一般提起python,就是指cpython
  • GIL的概念:
    GIL是cpython解释器的特点,其他版本没有这一特点。
    GIL的本质是一个加在python解释器上的互斥锁,用来阻止同一进程下的多个线程同时执行。
  • GIL的作用:
    因为cpython中的内存管理(GC)不是线程安全的,因此cpython通过GIL解决了这个问题(原理很复杂,不做深究)。
    虽然GIL导致cpython无法实现真正意义上的多线程运行(cpython是伪多线程),但从内存安全等多方面综合考虑,GIL的存在是利大于弊的。

实际上,cpython在早期就尝试过移除GIL,但结果是没有GIL的版本在性能方面,反而不如原来有GIL的版本,于是这个没有GIL的cpython版本最终被放弃。

十四、多进程与多线程比较

  • I/O密集型任务和计算密集型任务:
    I/O密集型任务是指要进行大量的I/O操作,大部分时间都在等待I/O操作完成,而对CPU占用较少的任务。
    计算密集型任务是指要进行大量的计算,消耗CPU资源,而I/O操作很少的任务。
  • python多进程:适合计算密集型。
  • python多线程:适合I/O密集型。