06.Python中的进程学习笔记

Python中的进程学习笔记

一级目录

二级目录

三级目录

进程基础的使用方法其实和线程极为类似,把线程学熟练了,进程也就手到擒来了

1.介绍进程:

1.进程的定义:

进程是一个执行中程序的实例。它是操作系统进行资源分配和调度执行的基本单位。

2.进程与程序的区别

  • 程序是一个静态的代码集合,它本身不能执行任何操作。
  • 进程是程序的动态执行实例,它具有分配给它的资源和正在进行的任务。

3.进程的状态:

工作中,任务数往往大于cpu的核数,即一定有一些任务正在执行,而另外一些任务在等待cpu进行执行,因此导致了有了不同的状态。

  • 就绪态:运行的条件都已经具备,正在等在cpu执行。
  • 执行态:cpu正在执行其功能。
  • 等待态:等待某些条件满足,例如一个程序sleep了,此时就处于等待态。

4.进程与线程:

线程是进程的执行单位,一个进程可以包含多个线程,它们共享进程资源但是可以并发执行。

5.进程隔离:

  • 每个进程在自己的地址空间中运行,因此它不能访问其他进程的内存。这种隔离保护了进程免受其他进程故障的影响。

2.进程的创建:

在Python中,可以使用multiprocessing模块来创建和管理进程。

import time
import multiprocessing

start = time.time()

def work_1():
    for _ in range(5):
        print("work_1")
        time.sleep(1)


def work_2():
    for _ in range(5):
        print("work_2")
        time.sleep(1)


p1 = multiprocessing.Process(target=work_1)
p2 = multiprocessing.Process(target=work_2)

if __name__ == '__main__':
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    final = time.time()
    print("输出总时间为:", final - start)

和线程类似,我们使用multiprocessing文件里的Process类来创建子进程。在这里p1p2Process类创建的两个实例对象。target通常指你希望并发执行的函数或方法。当你创建一个新的线程或进程,你需要指定这个线程或进程启动执行时应该调用哪个函数。这个函数就是被称为target的函数。(这里有一点需要说明的是,调用子线程的时候。最好写函数入口,即if __name__ == '__main__':,如果不这样的话,就需要手动调用底层的 fork()spawn() 方法,这两种方法取决于你的操作系统)

结果输出为:

D:\Anaconda\envs\myenv\python.exe D:\pythonProject\核心编程\06进程\01.进程的创建.py 
work_1
work_2
work_2
work_1
work_1work_2

work_2
work_1
work_2
work_1
输出总时间为: 5.2011730670928955

可以观察发现,两个子进程是同时执行的,实现了并发。

3.获取进程编号:

import os
import multiprocessing

def get_pid():
    print(f"当前子进程编号为:{os.getpid()},父进程编号为:{os.getppid()}")

if __name__ == '__main__':
    p = multiprocessing.Process(target=get_pid)
    p.start()
    p.join()
    print(f"当前主进程编号为:{os.getpid()}")
    print(f"pycharm进程编号为:{os.getppid()}")

输出结果:

D:\Anaconda\envs\myenv\python.exe D:\pythonProject\核心编程\06进程\02.获取进程的编号.py 
当前子进程编号为:101028,父进程编号为:88664
当前主进程编号为:88664
pycharm进程编号为:67104

在Python编程中,os是一个标准库模块,它提供了许多与操作系统交互的函数。os.getpid()os.getppid()分别是获取当前进程的编号和父进程的编号。不过需要注意的是,os.getppid() 获取的是创建当前进程的父进程ID,这在多层嵌套的进程产生过程中能明确看出各个层次的父子进程关系。所以在p这个子进程中,其父进程编号其实和主进程的编号是一样的。

4.进程的传参:

举一个批量下载图片的例子:

import multiprocessing
import requests

url_list = [
    'http://pic.bizhi360.com/bbpic/98/10798.jpg',
    'http://pic.bizhi360.com/bbpic/92/10792.jpg',
    'http://pic.bizhi360.com/bbpic/86/10386.jpg'
]


def download_img(url):
    try:
        response = requests.get(url, timeout=5).content  # 设置一个超时时间
        file_name = url.split('/')[-1]
        with open( file_name, mode='wb') as f:
            f.write(response)
            print('下载完成:', str(file_name))
    except requests.exceptions.RequestException as e:  # 异常处理
        print(f"请求失败,url:{url}, error:{e}")
    except Exception as e:  # 所有的异常都应该捕获并处理
        print(f"出错了,error:{e}")


if __name__ == '__main__':
    processes = []  # 创建一个列表来跟踪所有的进程

    for url in url_list:
        p = multiprocessing.Process(target=download_img, args=(url,))
        p.start()
        processes.append(p)

    for p in processes:  # 等待所有的进程完成
        p.join()

args=(url,)将包含单个元素(当前循环中的url)的元组传递给download_img函数。注意,args需要是一个元组,即使只有一个值。如果args设置的是(url),这不会被视为一个元组,而只是一个括号包围的普通变量。为了表示单元素元组,你需要在那个唯一的元素后面加一个逗号:(url,)。当子进程启动时,multiprocessing模块会将元组args中的每个元素传递给target函数。在你的例子里,这个函数就是download_img,它接受一个url作为输入。因此,每个子进程将调用download_img(url),其中url是从url_list中取出来的。

5.进程之间不共享数据:

为了保障数据安全和程序的稳定性。每个进程由操作系统分配独立的内存空间,其数据和状态都保存在各自的内存空间中,与其他进程的内存空间隔离开。

import multiprocessing

num_list = [1, 2, 3]
"""如果你在函数内部只是访问全局变量或对全局变量进行操作(如添加元素、修改元素等),而不改变全局变量的指向(即不使用赋值语句),那么不需要使用 global 关键字。"""
def work_1():
    for i in range(4,7):
        num_list.append(i)
    print('子进程1任务完成之后列表的元素为:', num_list)


def work_2():
    num_list.pop()
    print('子进程2任务完成之后列表的元素为:', num_list)


if __name__ == '__main__':
    p1 = multiprocessing.Process(target=work_1)
    p2 = multiprocessing.Process(target=work_2)


    p1.start()
    p2.start()


    p1.join()
    p2.join()

    print(num_list)

输出结果如下:

D:\Anaconda\envs\myenv\python.exe D:\pythonProject\核心编程\06进程\04.进程之间不共享数据.py 
子进程1任务完成之后列表的元素为: [1, 2, 3, 4, 5, 6]
子进程2任务完成之后列表的元素为: [1, 2]
[1, 2, 3]

6.队列的简单使用

1.在线程中使用队列的原因:
    避免线程之间资源竞争, 队列这种数据结构是线程安全的

2.在进程中使用队列的原因:
    队列在进程中可以实现资源共享
from queue import Queue


# 1.创建一个队列对象
queue = Queue(4)  # 创建了一个队列对象并设置队列对象存储的最大长度: 4个值

# 2.将数据传入到队列中
queue.put(1)

# 3.获取之前传递的数据
print(queue.get())

# 4.队列对象为空的情况下获取队列数据
print(queue.get())  # 如果队列为空会导致主线程堵塞, 直到队列中存在元素才会解堵塞

# 5.如果队列为空获取数据直接抛出异常,而不是出于死锁状态一直等待
print(queue.get_nowait())

# 6.判断队列为空,输出True/False
print(queue.empty())

# 7.设置队列的最大存储长度
for i in range(1, 5):
    queue.put(i)

# 8.如果队列已满则无法传递新的数据到队列中
queue.put(5)  # 如果队列已满则put方法会导致主线程堵塞

# 9.如果队列已满并且不想让主线程堵塞则使用put_nowait方法
queue_1.put_nowait(5)  # 导致主线程抛出异常

# 10.判断队列是否已满/空
print(queue.full())

print(queue.empty())

# 11.队列的特征: 先进先出
for _ in range(4):
    print(queue.get())

#  queue.qsize() 获取当前队列中的真实长度。
print(queue.qsize())

7.使用队列来共享数据

在Python中,可以使用multiprocessing模块中的Queue类来实现进程间的数据共享。Queue类是一个先进先出的队列,可以用来跨进程存储和共享对象。

import time, random
from multiprocessing import Queue, Process
# multiprocessing.Queue是用来在多个进程之间进行通信的;queue.Queue是设计用来在单个进程中的多个线程之间进行交互的

def write(queue):
    for value in [1, 2, 3, 4]:
        queue.put(value)
        time.sleep(random.random())


def read(queue):
    while True:
        if not queue.empty():
            value = queue.get()
            print(value)
            time.sleep(random.random())
        else:
            break

if __name__ == '__main__':
    q = Queue()

    p1 = Process(target=write, args=(q,))
    p2 = Process(target=read, args=(q,))

    p1.start()
    p1.join()

    p2.start()
    p2.join()

D:\Anaconda\envs\myenv\python.exe D:\pythonProject\核心编程\06进程\06.队列在进程中的使用.py 
1
2
3
4

8.用Joinablequeue实现队列信号传递

import random
from multiprocessing import Process, JoinableQueue
import time

def write(queue):
    for value in ['A', 'B', 'C']:
        queue.put(value)
        time.sleep(random.random()*2)

def read(queue):
    while True:
        value = queue.get()
        print("队列中的数据:",value)
        time.sleep(random.random())
        queue.task_done()  # 发送一个任务结束的信号
        """
        循环在队列中,取出一个就给主进程发送一个任务结束的信号
            直到这个队列为空
        """

if __name__ == '__main__':
    q = JoinableQueue()  # 在内部维护了一个计数器

    p1 = Process(target=write, args=(q,))
    p2 = Process(target=read, args=(q,))

    process_list = [p1, p2]
    for process in process_list:
        # 设置守护进程
        process.daemon = True
        process.start()

        # 由于 read 函数中的 while True 循环,read 进程会一直运行,不断地尝试从队列中读取数据,即使队列已经为空。这就导致了程序无法自然地结束,因为总会有一个非守护进程(read 进程)在运行。

    time.sleep(5)

    # 当队列计数器不为0时,会造成主进程堵塞
    q.join()
JoinableQueue是queue模块中的一个类,它是Queue的一个子类,添加了task_done()和join()方法。
task_done()方法通常在一个队列任务的工作者线程或进程结束一项工作后被调用,它表示Enqueue的项目已经被完全处理。
join()方法则是阻塞调用线程,直到队列中的所有项目都被处理并调用了task_done()方法。

9.进程池的创建与同步传递

进程池的创建和线程池的创建十分相似。

from  multiprocessing import Pool
import time
import random
import os

def work(message):
    p_start = time.time()
    print(f'{message}开始执行, 任务的进程编号为{os.getpid()}')
    time.sleep(random.random())
    p_end = time.time()
    print(f'{message}执行完毕, 耗时为: {p_end - p_start}')

if __name__ == '__main__':
    main_start = time.time()

    pool = Pool(3)

    for item in range(10):
        pool.apply(work, (item,))
    print('--------------------------------')
    pool.close()  # 告诉 Python 你已经完成了所有任务的提交,不希望再提交新的任务
    pool.join()  # pool.join() 就会阻塞主进程,等待所有的子进程任务都完成
    main_end = time.time()
    print("主进程耗时:", main_end - main_start)

# 运行结果和单线程其实没有任何区别,这是因为pool.apply(work, (item,))会造成主进程堵塞直到任务结束

Python的multiprocessing库提供了一个Pool类,用于管理进程池。进程池的好处是在程序运行过程中创建一定数量的进程,然后可以重复使用这些进程,避免了频繁创建和销毁进程的开销。Pool类有一个apply方法。这个方法的作用是将指定的函数作用于指定的参数,并返回函数的返回值。这个方法是阻塞的,也就是说,apply方法会一直等待直到对应的函数计算出结果。

Pool对象的closejoin方法经常会一起被使用。这两个方法通常在所有任务都被提交给Pool之后被调用。

  1. Pool.close(): 这个方法会阻止任何新的工作任务被提交到进程池。这意味着你已经完成了所有任务的提交,不再需要提交新的任务。这并不会立即关闭 Pool,还允许未完成的任务继续运行。
  2. Pool.join(): 这个方法被用来等待所有的工作进程完成。换句话说,主进程阻塞等待子进程的完成。但在调用此命令之前,必须先调用close()terminate()方法,否则会抛出异常。

这就像你在今天结束工作时。首先,你会告诉你的团队(对应Pool.close()),你今天不再给他们分配新的任务。然后,你会等待他们完成所有已分配的任务(对应Pool.join())。只有当所有的任务都完成了,你才可以安心下班。

10.进程池的创建与并发调度

如果你想要并行地执行多个任务,那么你应该使用apply_async方法,这个方法的使用方式和apply类似,但是它并不会等待结果返回,而是立即返回一个AsyncResult对象,你可以在以后使用这个对象的get方法来获取结果。

from  multiprocessing import Pool
import time
import random
import os

def work(message):
    p_start = time.time()
    print(f'{message}开始执行, 任务的进程编号为{os.getpid()}')
    time.sleep(random.random())
    p_end = time.time()
    print(f'{message}执行完毕, 耗时为: {p_end - p_start}')

if __name__ == '__main__':
    main_start = time.time()

    pool = Pool(3)

    for item in range(10):
        pool.apply_async(work, (item,))
        # apply_async 方法分发的任务(主人告诉厨师做的菜)是异步的,也就是说它们会在后台运行,不会阻塞主进程(派对不会因为等菜而停下.所以说有可能主进程都结束了,子进程还没结束,因此需要手动创建pool.close()和pool.close()
    print('--------------------------------')

    pool.close()  # 告诉 Python 你已经完成了所有任务的提交,不希望再提交新的任务
    pool.join()  # pool.join() 就会阻塞主进程,等待所有的子进程任务都完成

    main_end = time.time()
    print("主进程耗时:", main_end - main_start)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值