简记Python多进程知识点

一、进程创建的方式

1、fork

特点:

①直接拷贝当前进程中几乎所有变量的值(注意不是执行主进程中的代码);

②可以在代码中任何位置开启;

③可以自动拷贝文件对象,锁等特殊对象,也可以通过参数传递;

④只能在unix系统上使用, 注意不支持Windows;

⑤启动速度快;

为了说明第一点,通过看下述代码预测一下输出结果:

import multiprocessing


def task():
    file_obj.write("alex\n")
    file_obj.flush()


if __name__ == "__main__":
    multiprocessing.set_start_method("fork")
    file_obj = open("temp.txt", "a+", encoding="utf-8")
    file_obj.write("武佩齐\n")
    file_obj.flush()
    p1 = multiprocessing.Process(target=task)
    p1.start()

分析:

①主进程开启文件后file_obj.write("武沛齐\n")将数据写入内存,随后使用flush刷入磁盘; 此时磁盘中只有: 武沛齐

②开启新进程,新进程拷贝当前file_obj, 由于file_obj被flush过,此时的file_obj文件缓冲区是没有任何内容的,拷贝到新进程中的file_obj自然文件缓冲区也是没有内容的; 

③在新进程中, file_obj.write("alex\n")写入内存,同样执行flush刷入磁盘, 随后子进程结束,主进程在等待子进程结束后最终将file_obj对象关闭。

所以,可以得知,此时file_obj只有两行数据,就是"武沛齐\nalex\n"

2、spawn

特点:

①需要手动传入新进程需要的参数;

②只能在main中执行, 特别解释:  由于spawn开启的进程会自动执行开启进程前的代码, 如果不将开启进程的代码包裹在:

if __name__ == "__main__":
    pass

子进程就会误执行代码, 导致错误; 通过将代码包裹在在main中,子进程就不会执行main里的代码,从而保护了主进程的代码。

③与fork的③相反,但是注意,进程间可以传递进程锁;

④支持Windows系统和unix系统;

⑤启动速度慢;

特别为了说明第二点,我当时特地构造过一段代码研究,如下所示:

大家可以自己摸索执行,尝试解释一下为什么执行结果会长这样。

from multiprocessing import Process, set_start_method
import tkinter as tk


def open_new_process():
    array = (1, 2, 3)
    new_process = Process(target=process_task, args=(array, ))
    new_process.start()


def process_task(array: tuple[str | float]):
    print("正在处理进程任务......")
    return sum(array)


if __name__ == "__main__":
    set_start_method("spawn")

root = tk.Tk()
button = tk.Button(root, text="测试多进程按钮", command=open_new_process)
button.place(x=10, y=10)
root.mainloop()

3、forkserver

特点:

这个和fork很像,但是它只能运行在部分unix系统上,并且它开启新进程是基于空模版的;  由于我本人没有安装unix, 而且forkserver在实际项目中理应也极少使用,此处仅作了解。

二、进程间的资源共享, 此时简单介绍,具体用法"用查"

"""
进程间数据共享在multiprocessing中一共提供了四种方法
其中最常用的是①Queue和②Manager
还有③[C->Value+Array]和④Pipe的组合
③->严格遵循C语言对数据类型的定义,创建后的变量可以直接在进程间共享
④两个队列组成的管道, 1.主进程 -> 子进程 的队列 / 2.子进程 -> 主进程 的队列
队列由于其有序性,不会出现资源竞态问题,因此无需加锁
"""

文末有一个小项目,刚好使用了manager, 可以感受一下。

三、进程锁

由于比较懒,这里仅通过解释一段代码说明进程锁:

注意下述代码中,temp.txt当中有且仅有一行数据,里面的数值自定,我本人写的是15

"""
注意点:
①锁的对应:
multiprocessing创建的Process只能由multiprocessing.Lock/Rlock来锁;
concurrent.futures创建的进程池是由multiprocessing.Manager()的实例对象的Lock/Rlock来锁;
②进程锁可以在进程间共享,但线程锁不可以!
"""
import multiprocessing
import os
from time import sleep


def task(_lock: multiprocessing.RLock):
    _lock.acquire()
    # 此时简单的将文件中的数字作为进程间的共享变量
    with open("temp.txt", "r+", encoding="utf-8")as file:
        current_ticket_count = int(file.read())
        print("排队抢票啦!")
        sleep(0.5)
        current_ticket_count -= 1
        # 一定要记住,文件操作后文件指针也发生了偏移,要用seek归位
        # 默认truncate后指针位置不变; 由于原来使用了file.read(),指针当前指向文件末尾;
        # 但是文件又是清空了的,再从原来位置写入数据就是访问不存在区域,自然会报错
        file.truncate(0)
        file.seek(0)
        file.write(str(current_ticket_count))
    _lock.release()


if __name__ == "__main__":
    multiprocessing.set_start_method("spawn")
    _lock = multiprocessing.RLock()
    for _ in range(os.cpu_count() - 1):
        # 进程锁是可以用args共享的
        p = multiprocessing.Process(target=task, args=(_lock, ))
        p.start()

分析:

①注意观察到task()函数,如果多进程下同时运行该函数,并且不加锁的话,存在这样一种情况:A进程刚读完file中的内容,+1后准备写入,但此时B进程快人一步,已经提前写入新的数据;此时A进程再写入,结果就覆盖了B进程刚写入的数据; 因此必须要加锁!

②注意此时使用的锁是multiprocessing.Rlock, 这是进程锁而非线程锁;

其实仔细想想也可以理解,如果把主进程的主线程锁交给子进程,多少有点慌;因为线程粒度比进程小得多,你将控制精度更小的线程交给一个甚至有进程风险的子线程保管,显然是极其危险的。

四、进程池

特别注意一点,在Python中,multiprocessing模块和concurrent.futures模块都提供了进程池,

但是为了代码风格的统一(毕竟线程池也不是threading模块提供的,而是concurrent.futures模块提供的),我们最好使用concurrent.futures中的进程池。

进程池和线程池的用法几乎没有差异,唯一要注意的一点是进程池里Futures对象的add_done_callback执行的回调函数是在主进程中进行的!

下面通过一个项目简单体会一下进程池,

项目目标,统计各个日志文件中访问量以及访问人数(不重复的ip数)

最终打印出形如这样的字典:

{"20240918": {"total": 120339, "ip": 87322}, ......}

①首先生成虚拟数据, 请先在当前目录下创建一个名为"resourses"的文件夹,随后在文件夹中创建一个Python文件,复制以下代码,生成数据, 注意要自行修改每个要生成的日志条数以及日志文件名,尽量多生成一些日志文件,这样在后面多进程中才能体现出多进程的提速效果。

import random
from datetime import datetime, timedelta


def get_random_ip():
    global last_id
    # 随机生成IP地址
    repeat = random.randint(0, 1)
    if not repeat:
        last_id = '.'.join(str(random.randint(0, 255)) for _ in range(4))
    return last_id


# 随机生成User Agent
user_agents = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8",
    "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0",
    "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1",
    "Mozilla/5.0 (iPad; CPU OS 9_3_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) Version/9.0 Mobile/13F69 Safari/601.1"
]


def get_random_user_agent():
    return random.choice(user_agents)


# 随机生成访问时间
def get_random_date(start_year=2015, end_year=2024):
    start_date = datetime.strptime(f"{start_year}-01-01", "%Y-%m-%d")
    end_date = datetime.strptime(f"{end_year}-12-31", "%Y-%m-%d")
    delta_days = (end_date - start_date).days
    random_days = random.randrange(delta_days)
    return (start_date + timedelta(days=random_days)).strftime("%d/%b/%Y:%H:%M:%S")


# 生成日志条目
def generate_log_entry():
    ip = get_random_ip()
    user_agent = get_random_user_agent()
    date = get_random_date()
    request = f"GET / HTTP/1.1"
    status = random.choice([200, 301, 404, 500])
    size = random.randint(100, 1500)
    return f"{ip} - - [{date}] \"{request}\" {status} {size} \"{user_agent}\"\n"


def generate_logs(filename, num_entries=1000):
    # 生产日志文件
    with open(filename, 'w') as file:
        for _ in range(num_entries):
            log_entry = generate_log_entry()
            file.write(log_entry)


if __name__ == "__main__":
    last_id = "118.199.64.14"
    log_filename = "20240923.log"
    generate_logs(log_filename, 136745)

②在当前目录下创建Python文件,执行下属代码:

(这份代码我个人认为已经没有需要解释的地方了,写得非常清晰, 自行体会吧)

"""
语法几乎和线程池没有差异,唯一需要注意的是进程池里的回调函数add_done_callback
不是在子进程中完成的,而是由主进程调度完成
案例: 计算每天用户访问情况
"""
from concurrent import futures
from multiprocessing import Manager
from os import scandir, path, cpu_count
from time import time


def timeit(func):
    def wrapper(*args, **kwargs):
        start_time = time()
        result = func(*args, **kwargs)
        end_time = time()
        print(f"{func.__name__} took {end_time - start_time} seconds to execute.")
        return result
    return wrapper


def task(log_file_path, share_dict):
    ip_set = set()
    log_dict = {"total": 0, "ip": 0}
    log_file_name = path.basename(log_file_path)

    with open(log_file_path, "r", encoding="utf-8") as f:
        for log_data in f:
            ip = log_data.split(" - - ", )[0]
            ip_set.add(ip)
            log_dict["total"] += 1

    log_dict["ip"] = len(ip_set)
    share_dict[log_file_name] = log_dict


def task2(log_file_path):
    ip_set = set()
    log_dict = {"total": 0, "ip": 0}

    with open(log_file_path, "r", encoding="utf-8") as f:
        for log_data in f:
            ip = log_data.split(" - - ", )[0]
            ip_set.add(ip)
            log_dict["total"] += 1
    log_dict["ip"] = len(ip_set)
    return log_dict


def callback_func(share_dict, file_name):
    def inner(future: futures.Future):
        share_dict[file_name] = future.result()

    return inner


@timeit
def single_process():
    share_dict = dict()
    for dir_entry in scandir("resources"):
        task(dir_entry.path, share_dict)
    print(share_dict)


@timeit
def multi_process_no_callback():
    manager: Manager = Manager()
    share_dict = manager.dict()
    # 直接榨干cpu哈哈哈
    with futures.ProcessPoolExecutor(max_workers=cpu_count()-1)as pool:
        for dir_entry in scandir("resources"):
            pool.submit(task, dir_entry.path, share_dict)

    print(share_dict)
    manager.shutdown()  # 手动关闭manager共享资源对象


@timeit
def multi_process_callback():
    # 使用回调函数,可以不创建共享资源对象manager, 通过callback函数在主进程中将子进程的结果汇总
    share_dict = dict()
    with futures.ProcessPoolExecutor(max_workers=cpu_count()-1)as pool:
        for dir_entry in scandir("resources"):
            future: futures.Future = pool.submit(task2, dir_entry.path)
            future.add_done_callback(callback_func(share_dict, dir_entry.name))
    print(share_dict)


if __name__ == "__main__":
    # single_process() 0.9s
    #  0.68s multi_process_no_callback()  主要是进程调度还是很耗时的,因此就这点任务量还不足以体现多进程的优势
    # 综合比较,不创建共享资源对象使用回调函数的方式是最高效的! 耗时0.48s
    multi_process_callback()

Python的turtle库是一个海龟绘图模块,它是Python自带的标准库,可以通过import语句导入并在Python中使用。海龟绘图源自20世纪60年代的logo语言,它模拟了一个真实的海龟在画布上游走的过程,通过控制海龟的移动和绘画来创建图形。turtle库提供了一些功能函数,包括移动和绘画等操作。你可以使用turtle.fd(d)向前移动一定的距离d,使用turtle.bk(d)向后移动一定的距离d,使用turtle.circle(半径, 弧度)以某个点为圆心绘制曲线等。此外,turtle库还提供了绝对坐标和海龟坐标两种角度坐标体系,你可以通过turtle.seth(angle)来改变海龟的游走方向,也可以通过turtle.left(angle)和turtle.right(angle)以海龟为参考系改变方向。下面是一个示例代码: import turtle turtle.left(45) turtle.fd(150) turtle.right(135) turtle.fd(300) turtle.left(135) turtle.fd(150) 这段代码可以让海龟按照一定的角度和距离绘制出一些图形。通过使用turtle库,你可以使用海龟绘图来进行一些简单的图形绘制和可视化操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Python基础学习简记--海龟绘图(Day10)](https://blog.csdn.net/weixin_39344224/article/details/102807350)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [python 学习笔记(三)---turtle库的使用(超详细)](https://blog.csdn.net/qq_40181592/article/details/86770960)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值