1.2.进程和线程

1.2.1.进程

python实现多进程的方法主要有两种:

(1)使用os模块中的fork,在linux实现,Windows不支持

(2)使用multiprocessing模块,跨平台

1.2.1.1.os==>fork

        fork方法是调用一次,返回两次。

        原因:操作系统将当前进程,进行复制,于是fork在当前进程(父),和复制进程(子)中返回。子进程永远返回0,父进程中返回的是子进程的ID。

import os

def child_process():
    print("Child process")
    print("PID:", os.getpid())

def parent_process():
    print("Parent process")
    print("PID:", os.getpid())
    
    # 创建子进程
    pid = os.fork()
    
    if pid == 0:
        # 子进程执行路径
        child_process()
    else:
        # 父进程执行路径
        print("Child PID:", pid)

# 主进程执行路径
parent_process()
1.2.1.2.使用multiprocessing模块创建多进程
import multiprocessing

def child_process():
    print("Child process")
    print("PID:", multiprocessing.current_process().pid)

def parent_process():
    print("Parent process")
    print("PID:", multiprocessing.current_process().pid)
    
    # 创建子进程
    child = multiprocessing.Process(target=child_process)
    child.start()
    child.join()

if __name__ == '__main__' :
    # 主进程执行路径
    parent_process()

        在这个示例中,我们使用multiprocessing.Process类来创建子进程,并且将子进程的执行逻辑定义为child_process函数。在parent_process函数中,我们创建了子进程对象child,并通过start()方法启动子进程,然后使用join()方法等待子进程执行完成

        运行上述代码,将会输出父进程和子进程的进程ID(PID)。子进程从child.start()调用处开始执行,并且输出的PID为子进程的进程ID,而父进程输出的PID则为主进程的进程ID。

1.2.1.3.multiprocessing提供了一个Pool类来代表进程池对象

        要是启动大量的进程,使用进程池批量的创建子进程的方式更加常见。

        Pool可以提供指定数量的进程供用户使用,默认大小是cpu的核数。

        当有新的请求提交到Pool中,如果池中还没满,那么就会创建一个新的进程来执行该请求;

如果已经达到了最大进程数,那么该请求就会等待,知道池中有进程结束,才会创建新的进程来处理它。

import multiprocessing

# 计算平方的函数
def square(x):
    return x ** 2

if __name__ == '__main__':
    # 创建一个拥有4个进程的进程池
    with multiprocessing.Pool(processes=4) as pool:
        # 调用`map`方法并行计算结果
        result = pool.map(square, [1, 2, 3, 4, 5])

    # 输出计算结果
    print(result)

        在这个例子中,我们首先定义了一个函数square,用于计算给定数值的平方。

        接下来,在if __name__ == '__main__':中的代码块中,我们创建了一个拥有4个进程的进程池pool。通过指定processes参数,我们告诉Pool类要创建的进程数量。然后,我们使用pool.map方法并行计算给定列表中每个元素的平方。map方法会将列表中的元素分配到不同的进程中进行计算,并返回计算结果。

        最后,我们输出计算结果。

        其中:pool.map(square, [1, 2, 3, 4, 5])的执行过程

  1. pool.map方法接收两个参数:要应用的函数square和包含输入参数的可迭代对象[1, 2, 3, 4, 5]

  2. Pool类根据自身拥有的进程数量,将输入参数列表中的元素分配到不同的进程中。

  3. 对于本例中的输入列表[1, 2, 3, 4, 5],假设进程池中有4个进程,那么进程池会把这5个元素分配给4个进程进行处理。具体分配情况可能是 [1, 2][3, 4][5]

  4. 每个进程将函数square应用到其分配到的元素上,计算出结果。这意味着每个进程都会执行square函数,分别对其分配到的元素进行平方计算。

  5. 各个进程独立地执行计算,并行地进行工作。

  6. 当所有进程都完成计算后,Pool类会将各个进程的计算结果合并成一个整体,并返回最终的结果列表。

  7. 最终的计算结果将被存储在变量result中(在此例中没有显示代码),你可以使用这个结果来进行后续的操作。

        ★★★通过这种方式,Pool.map方法实现了并行计算,充分利用了多核处理器的优势,从而加快了整体的计算速度。

        还有一个函数apply_async(),与map()区别

  1. map() 函数:

    • map() 函数用于将一个函数应用于一个可迭代对象(如列表、元组等)的所有元素,并返回一个包含结果的列表。
    • 它是一种同步操作,会阻塞主进程,直到所有任务完成。
    • map() 适用于简单的并行计算,不需要关心任务的完成顺序。
  2. apply_async() 函数:

    • apply_async() 函数用于异步地提交一个任务到进程池,并返回一个 AsyncResult 对象。
    • 它不会阻塞主进程,可以继续执行其他操作。
    • apply_async() 提供了更多的灵活性,可以通过 AsyncResult 对象获取任务的状态、结果等信息。
    • 可以根据需要设置任务的回调函数,以便在任务完成时进行进一步的处理。
import multiprocessing

# 计算平方的函数
def square(x):
    return x ** 2

if __name__ == '__main__':
    # 创建一个拥有4个进程的进程池
    with multiprocessing.Pool(processes=4) as pool:
        # 调用`apply_async`方法异步计算结果
        result1 = pool.apply_async(square, (10,))
        result2 = pool.apply_async(square, (20,))

        # 等待计算完成并获取结果
        res1 = result1.get()
        res2 = result2.get()

    # 输出计算结果
    print(res1, res2)

        在这个例子中,我们首先定义了一个函数square,用于计算给定数值的平方。

        接下来,在if __name__ == '__main__':中的代码块中,我们创建了一个拥有4个进程的进程池pool。然后,我们使用apply_async方法异步计算结果,并且传递给函数的参数是(10,)(20,),分别代表计算10的平方和计算20的平方。

        最后,我们使用AsyncResult.get()方法获取异步计算的结果,并存储在变量res1res2中。最终,我们输出这两个结果。

1.2.1.4.进程间通信

        python提供了多种进程间通信方式:queue,pipe,value+arrary等

        我们主要对queue和pipe进行讨论。

        1.2.1.4.1.Queue

                queue是多进程间安全的队列。有两个方法put和get

                在使用 Queue 进行进程间通信时,你可以使用 put 方法将数据放入队列,使用 get 方法从队列中获取数据。

方式 参数特点
put

・有两个可选参数blocked和timeout

・如果blocked为true(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。

・如果超时,会抛出Queue.Full异常。

・如果blocked为false,但该queue已满,会立刻抛出Queue.Full异常。

get

・可以从队列中读取并删除一个元素。

・有两个可选参数blocked和timeout

・如果blocked为true(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。

・如果blocked为false(1)有一个可用的值,则立刻返回;(2)如果队列为空,则会立刻抛出Queue.Empty异常。

from multiprocessing import Process, Queue

# 定义一个函数,用于向队列中放入数据
def producer(queue):
    for i in range(3):
        item = "Message {}".format(i)
        queue.put(item)  # 将数据放入队列
        print("Produced:", item)

# 定义一个函数,用于从队列中获取数据
def consumer(queue):
    while True:
        item = queue.get()  # 从队列中获取数据
        if item is None:
            break
        print("Consumed:", item)

if __name__ == "__main__":
    queue = Queue()  # 创建一个队列

    # 创建两个进程,一个用于生产数据,一个用于消费数据
    producer_process = Process(target=producer, args=(queue,))
    consumer_process = Process(target=consumer, args=(queue,))

    producer_process.start()
    consumer_process.start()

    producer_process.join()
    queue.put(None)  # 发送结束信号
    consumer_process.join()

queue.put(None)解析

        主进程向队列中放入结束标志,通知消费者进程数据已经全部生产完毕。

        1.2.1.4.2.Pipe

                在 Python 的 multiprocessing 模块中,Pipe 主要用于在父进程和子进程之间进行通信,因此它通常用于父子进程之间的通信。但是,如果有多个子进程,它们之间也可以通过 Pipe 进行通信

例:父==》子,发消息

from multiprocessing import Process, Pipe

# 定义一个函数,用于在子进程中执行
def child_process(conn):
    data = conn.recv()  # 子进程从管道中接收数据
    print("Child process received:", data)

if __name__ == '__main__' :
    # 创建管道
    parent_conn, child_conn = Pipe()

    # 创建子进程,并将管道传递给子进程
    p = Process(target=child_process, args=(child_conn,))
    p.start()

    # 父进程向管道中发送数据
    parent_conn.send("Hello from parent process!")

    p.join()

例:子==》父,发消息

from multiprocessing import Process, Pipe

# 定义一个函数,用于在子进程中执行
def child_process(conn):
    message = "Message from child process"
    conn.send(message)  # 子进程向管道中发送消息

if __name__ == '__main__' :
    # 创建管道
    parent_conn, child_conn = Pipe()

    # 创建子进程,并将管道传递给子进程
    p = Process(target=child_process, args=(child_conn,))
    p.start()

    # 父进程从管道中接收消息
    received_message = parent_conn.recv()
    print("Parent process received:", received_message)

    p.join()

★★★关于Pipe()参数

Pipe() 方法可以接受一些参数来配置创建管道的行为。具体来说,Pipe() 方法可以接受两个参数:duplexctx

  1. duplex 参数:表示管道的类型,如果设置为 True,则创建全双工管道;如果设置为 False,则创建半双工管道默认值为 True,即创建全双工管道。

  2. ctx 参数:表示使用的上下文对象,通常情况下不需要显式地指定这个参数。如果需要在特定的上下文中创建管道,可以传递一个 multiprocessing.context 对象作为这个参数的值。

1.2.2.多线程

        python的标准库提供了两个模块:thread和threading,thread是低级模块,threading是高级模块,对thread进行了封装。绝大多数情况下,我们只需要使用threading这个模块。

1.2.2.1.用threading模块创建多线程

        threading通过两种方式创建多线程:

        (1)方式一,把一个函数传入并创建thread实例,然后调用start方法开始执行

        (2)方式二,直接从threading.thread继承并创建线程类,然后重写__init__方法和run方法

例(1)

import threading

# 定义一个函数
def my_function():
    print("This is my function.")

# 创建 Thread 实例,将函数作为参数传入
my_thread = threading.Thread(target=my_function)

# 调用 start() 方法开始执行线程
my_thread.start()

# 等待线程执行完毕
my_thread.join()

print("Thread finished.")

        在 Python 中,当调用线程的 start() 方法启动线程后,线程会在后台运行,并不会阻塞主线程的执行。如果希望主线程能够等待子线程完成后再继续执行,可以使用 join() 方法,它将会阻塞主线程,直到子线程执行完毕

        因此,为了保证线程执行的正确性,通常情况下都需要在启动线程后调用 join() 方法来等待线程执行完毕。

例(2)

import threading

# 自定义线程类
class MyThread(threading.Thread):
    def __init__(self, name):
        # 调用父类的 __init__() 方法
        super().__init__()
        self.name = name

    def run(self):
        print("Thread", self.name, "is running")

# 创建自定义线程实例
my_thread = MyThread("CustomThread")

# 调用 start() 方法开始执行线程
my_thread.start()

my_thread.join()
1.2.2.2.线程同步

        如果多个线程共同对某个数据进行修改,则可能产生不可预料的结果,为了确保数据的正确性,需要对多个线程进行同步。使用Thread的Lock和Rlock可以实现简单的线程同步。

        两个对象都有acquire和release方法,对于那些每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间。

        对于lock对象而言,如果一个线程连续两次进行acquire操作,那么由于第一次acquire后没有release操作,第二次acquire将挂起线程。这会导致lock对象,将永远不会release,使得线程死锁。

        RLock对象允许一个线程多次对其进行acquire操作,因为在其内部通过一个counter变量维护着线程acquire的次数。而且每一次的acquire的操作必须有一个release操作与之对应,在所有的release操作完成之后,别的线程才能申请该RLock对象。

例:

import threading

# 创建一个可重入锁
rlock = threading.RLock()

def example_function():
    with rlock:
        print(f"{threading.current_thread().name}获取到了锁")
        nested_function()

def nested_function():
    with rlock:
        print(f"{threading.current_thread().name}再次获取到了锁")

# 创建多个线程来调用 example_function
threads = []
for i in range(3):
    thread = threading.Thread(target=example_function)
    threads.append(thread)
    thread.start()

# 等待所有线程执行完毕
for thread in threads:
    thread.join()

print("所有线程执行完毕")
1.2.2.3.全局解释器锁(GIL)

        在 Python 中,全局解释器锁(Global Interpreter Lock,简称 GIL)是一种机制,它确保在任何时刻只有一个线程在解释器中执行 Python 字节码。这意味着在多线程的情况下,即使有多个线程,它们也不能同时利用多个 CPU 核心。GIL 在 CPython 中实现,CPython 是 Python 的标准实现。

        GIL 的存在是为了简化 CPython 的内存管理和对象模型。由于 GIL 的存在,CPython 在处理多线程程序时并不能完全发挥多核 CPU 的优势。因为在多线程环境下,即使有多个线程,它们也只能交替执行,而不能真正地并行执行。

        需要注意的是,GIL 只对解释器层级的操作产生影响,对于 I/O 操作、调用 C 扩展模块等场景,GIL 不会造成影响,因为这些情况下会释放 GIL

        由于 GIL 的存在,Python 在 CPU 密集型任务上的性能表现可能不如预期,但在 I/O 密集型任务上,由于 GIL 不会产生阻碍,Python 仍然可以表现出良好的性能。

        虽然 GIL 在某些情况下会成为性能瓶颈,但它也带来了一些好处,比如简化了 CPython 的内存管理,使得 CPython 更容易设计和维护。另外,对于单核和 I/O 密集型任务,GIL 并不会带来太大的问题。

总之,全局解释器锁 GIL 是 Python 解释器中的一种机制,它限制了多线程程序的并行执行,因此在 CPU 密集型任务上可能会影响性能。在实际开发中,针对不同类型的任务,可以根据需求选择合适的并发方案,比如使用多进程、使用异步编程等来规避 GIL 的影响

1.2.3.协程

        Python 的协程是一种轻量级的并发编程方式,它允许在单线程中实现多个协作任务的并发执行。协程通过使用特殊的关键字 yieldasync/await 实现,这些关键字可以将函数暂停和恢复执行

        Python 3.5 引入了 asyncio 模块,它提供了对协程的支持,使得异步编程更加方便。

        协程(coroutine),又称微线程,纤程,是一个用户级的轻量级的线程。协程拥有自己的寄存器上下文和栈。

        协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。

        在并发操作中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其他协程共享全局数据和其他资源。

        协程需要用户自己编写调度逻辑,对于cpu来说,协程其实是单线程,所以cpu不用考虑怎么去调度、切换上下文,这就省去了cpu的切换开销,所以协程一定程度上又好于多线程。

        python通过yield提供了对协程的基本支持,但不完全。而使用第三方gevent库是更好的选择,gevent提供了比较完善的协程支持。

1.2.3.1.yield和gevet区别

  yieldgevent 都与协程有关,但它们有不同的作用和使用方式。

  1. yield:

    • yield 是 Python 中用于生成器函数的关键字,它可以将函数转变成一个生成器。通过 yield,可以在函数内部实现暂停和恢复执行,使得函数可以在中间状态暂停,并返回一个值给调用方。
    • 通过 yield 关键字,我们可以实现简单的协程,进行协作式多任务处理。在 Python 中,yield 被广泛用于实现异步编程模型,比如通过生成器实现的异步任务调度。
  2. gevent:

    • gevent 是一个基于协程的 Python 网络库,它提供了一个基于 libev 或 libuv 的事件循环,并且使用协程来实现并发。通过 gevent,可以在网络编程中实现高效的并发处理,而无需显式地编写多线程或多进程代码。
    • gevent 提供了一个 monkey 模块,可以将标准库中的阻塞式 I/O 操作替换为协作式的操作,从而实现非阻塞的并发编程。
    • 通过 gevent,我们可以使用 spawnjoinall 等方法来创建和管理协程,实现异步的网络编程和并发处理。

总的来说,yield 是 Python 中用于生成器函数的关键字,用于实现简单的协程,而 gevent 是一个基于协程的网络库,用于实现高效的并发和异步编程。在实际的开发中,可以根据具体的需求选择合适的工具和技术来实现并发和异步处理。希望这能够帮助到你理解它们的区别

例.yield

def fibonacci_generator(n):
    a, b = 0, 1
    count = 0
    while count < n:
        yield a
        a, b = b, a + b
        count += 1

# 创建一个斐波那契数列生成器
fibonacci_gen = fibonacci_generator(10)

# 使用循环迭代生成器并打印值
for num in fibonacci_gen:
    print(num)

当调用 fibonacci_generator(n) 时,它会返回一个生成器对象。该生成器对象可以用来迭代生成斐波那契数列的前 n 项。

  1. 在函数开始时,我们初始化两个变量 ab 分别为 0 和 1,以及一个计数器 count 置为 0。

  2. while count < n: 是一个循环条件,它判断计数器 count 是否小于 n。只要条件满足,循环就会继续执行。

  3. 在循环内部,我们使用 yield a 将当前的斐波那契数 a 返回给调用方,并且暂停函数的执行。这意味着每次迭代时,我们可以得到一个斐波那契数值

  4. 接下来,我们通过 a, b = b, a + b 更新 ab 的值,使其分别成为上一次斐波那契数列的第二项和第一项的和。

  5. count += 1 将计数器 count 增加 1,表示已经生成了一项斐波那契数。

  6. 循环会继续执行,直到计数器 count 达到 n,此时循环退出。

  7. 如果之后还有调用生成器,它将从上一次暂停的位置恢复执行,继续生成下一个斐波那契数值,并再次暂停。

【安装gevent命令】

        (1)pip install gevet

        (2)pip install -i https://pypi.tuna.tsinghua.edu.cn/simple gevent
例.gevent

import gevent
from gevent import monkey
import time

# 打开猴子补丁,使得gevent能够识别常见的阻塞操作,如网络请求
monkey.patch_all()

def compute_task(task_name, duration):
    print(f"Starting task: {task_name}")
    time.sleep(duration)  # 模拟耗时计算
    print(f"Task {task_name} completed")

# 创建并启动协程
tasks = [
    gevent.spawn(compute_task, "Task 1", 3),
    gevent.spawn(compute_task, "Task 2", 2),
    gevent.spawn(compute_task, "Task 3", 4)
]

gevent.joinall(tasks)
  1. 导入必要的模块:代码首先导入了 geventmonkey 模块,然后调用了 monkey.patch_all() 方法打开猴子补丁,以便 gevent 能够识别常见的阻塞操作,如网络请求。

  2. 定义耗时计算任务函数:接下来定义了一个名为 compute_task 的函数,该函数模拟了一个耗时的计算任务。在这个例子中,compute_task 函数接受两个参数,分别是任务名称 task_name 和任务执行的持续时间 duration。函数首先打印任务开始的消息,然后通过 time.sleep(duration) 来模拟任务执行的耗时,最后打印任务完成的消息。

  3. 创建并启动协程:在创建协程之前,我们将三个耗时计算任务分别放入 tasks 列表中,并使用 gevent.spawn 方法创建了三个协程对象,每个协程对象都对应一个耗时计算任务。这样做可以确保这些计算任务可以并发执行。接着,我们使用 gevent.joinall(tasks) 来等待所有的协程执行完成。

        当程序运行时,gevent 会自动切换协程的执行,让它们在耗时操作时不会阻塞整个程序,从而实现了并发执行。由于打开了猴子补丁,gevent 可以识别并处理 time.sleep 这样的阻塞操作,因此可以正确地进行协程调度。

        总之,这段代码演示了如何使用 gevent 实现并发执行耗时计算任务的协程,通过合理的协程调度,避免了阻塞式的同步执行,提高了程序的执行效率

        gevent是基于协程的python网络函数库,使用greenlet在libev事件循环顶部提供了一个有高级别并发性的api。

主要特性

基于libev的快速事件循环,Linux上是epoll机制(※1)
基于greenlet的轻量级执行单元
api复用了python标准库里的内容
支持ssl的协作式sockets
可通过线程池或c-ares实现DNS查询
通过monkey patching功能使得第三方模块变成协作式

※1:

当说到“基于libev的快速事件循环,Linux上是epoll机制”时,涉及到了几个重要的概念。让我逐一为你详细解释:

  1. libev:libev 是一个用于事件循环的开源库,它提供了跨平台的事件驱动编程接口,并且在不同操作系统上使用不同的底层事件通知机制来实现高效的事件循环。在 Linux 系统上,libev 使用的是 epoll 机制来实现事件通知和事件驱动。

  2. 事件循环:事件循环是一种编程模型,用于处理异步事件和 I/O 操作。在事件循环中,程序会不断地监听事件,当事件发生时,会触发相应的回调函数进行处理。事件循环可以高效地处理大量的并发连接和 I/O 操作,是实现高性能网络服务的重要组成部分。

  3. epoll:epoll 是 Linux 下的一种高性能的事件通知机制,用于处理大量文件描述符的 I/O 事件。相比于传统的 select 和 poll 方法,epoll 具有更好的性能和扩展性,能够有效地应对大规模并发连接的情况。在 Linux 上,许多网络编程框架和库都会使用 epoll 来实现高性能的事件驱动编程。

因此,当说“基于libev的快速事件循环,Linux上是epoll机制”时,意味着 libev 在 Linux 系统上使用了 epoll 机制来实现高效的事件通知和事件驱动,从而提供了快速的事件循环能力,适用于构建高性能的异步 I/O 应用程序。

gevent对协程的支持,本质上是greenlet在实现切换工作。

在 Python 中,greenlet 是一种协程库,它提供了一种基于用户空间的轻量级协程实现。下面是 greenlet 的工作流程:

  1. 创建和切换协程:首先,你可以使用 greenlet 库创建多个协程(也称为 greenlet 对象)。每个协程都代表一个独立的执行流。当你需要切换到其他协程时,可以通过调用 greenlet.switch() 方法来手动进行切换。

  2. 执行和暂停:当一个协程开始执行时,它会一直执行直到遇到显式的切换点或者遇到阻塞操作在遇到切换点时,程序可以选择切换到其他协程继续执行,而不是等待当前协程执行完毕

  3. 手动切换:通过调用 greenlet.switch() 方法,你可以在任何时候手动触发协程之间的切换。这种方式下,协程的切换是由程序员显式控制的,这使得协程之间的调度更加灵活。

  4. 结合事件驱动框架:通常情况下,greenlet 会和其他事件驱动框架(比如 gevent)结合使用,以实现高效的并发编程。在这种情况下,greenlet 被用作底层的协程实现,而高级的并发模型则由事件驱动框架来管理。

总体来说,greenlet 工作流程可以概括为:创建多个协程,手动控制协程之间的切换,以及结合其他框架来实现高效的并发编程。通过这种方式,程序员可以更加灵活地管理多个执行流,实现非阻塞的并发操作。

greenlet是一种合理安排的串行方式。

        由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO,这就是协程一般比线程效率高的原因。

        由于切换是在io操作时自动完成的,所以gevent需要修改python自带的一些标准库,所以Greenlet需要修改Python自带的一些标准库,将一些常见的阻塞,如socket、select等地方实现跳转,这一过程启动通过monkey patch完成。

        gevent中还提供了对池的支持。当拥有动态数量的greenlet需要进行并发管理(限制并发数量)时,就可以使用池。这在处理大量的网络和IO操作时是非常需要的。接下来使用gevent中pool对象。

例.gevent pool

import gevent
from gevent.pool import Pool

# 定义一个模拟的任务函数
def task(name):
    print('Starting task', name)
    gevent.sleep(1)  # 模拟任务执行时间
    print('Completed task', name)

# 创建一个协程池,限制同时执行的协程数量为2
pool = Pool(2)

# 添加多个任务到协程池
for i in range(5):
    pool.spawn(task, i)

# 等待所有任务完成
pool.join()

        在这个例子中,首先导入了 gevent 和 Pool 对象。然后定义了一个模拟的任务函数 task(),它会打印任务开始和结束的信息,并且使用 gevent.sleep() 来模拟任务的执行时间。

        接下来,创建了一个协程池对象 pool,通过 Pool(2) 指定限制同时执行的协程数量为 2。

        然后使用 pool.spawn() 方法将多个任务添加到协程池中,这些任务会在有空闲协程时被调度执行。

        最后,调用 pool.join() 方法等待所有任务完成。在执行过程中,最多同时有两个任务在执行,其他任务会在有空闲协程时被调度执行。

        通过使用 gevent 的 Pool 对象,我们可以方便地控制并发执行的协程数量,从而实现高效的任务调度和执行。

​​​1.2.4.分布式进程

        分布式进程是指将process进程分布到多台机器上,充分利用多台机器的性能完成复杂的任务。

        分布式进程在python中依然要用到multiprocessing模块。multiprocessing不但支持多进程,其中messages子模块还支持把多进程分布到多台机器上。

        可以写一个服务进程作为调度者,将任务分布到其他多个子进程中,依靠网络通信进行管理。

        下面介绍两种常用的分布式进程实现方式。

        1.2.4.1.multiprocessing 模块

        Python 的标准库中的 multiprocessing 模块提供了一种简单的分布式进程实现方式。该模块可以让你创建和管理多个进程,并且支持进程间的通信。你可以使用 multiprocessing.Pool 对象来创建一个进程池,然后使用 apply()map() 等方法将任务分发给进程池中的多个进程进行并行处理。

        举个例子:
        在做爬虫程序时,常常会遇到这样的场景,我们想抓取某个网站的所有图片,如果使用多进程的话,一般是一个进程负责抓取图片的链接地址,将链接地址存放到Queue中,另外的进程负责从Queue中读取链接地址进行下载和存储到本地。
        现在把这个过程做成分布式,一台机器上的进程负责抓取链接,其他机器上的进程负责下载存储。
        那么遇到的主要问题是将Queue暴露到网络中,让其他机器进程都可以访问,分布式进程就是将这一个过程进行了封装,我们可以将这个过程成为本地队列的网络化。

任务进程,创建分布式进程需要分为6个步骤:
(1)建立队列Queue,用来进行进程间的通信。服务进程创建任务队列task_queue,用来作为传递任务给任务进程的通道;服务进程创建结果队列result_queue,作为任务进程完成任务后回复服务进程的通道。在分布式多进程环境下,必须通过由Queuemanager获得Queue接口来添加任务。
(2)把第一步中建立的队列在网络上注册,暴露给其他进程(主机),注册后获得网络队列,相当于本地队列的映像。
(3)建立一个对象(Queuemanager(BaseManager))实例manager,绑定端口和验证口令。
(4)启动第三步中建立的实例,即启动管理manager,监管信息通道。
(5)通过管理实例的方法获得通过网络访问的Queue对象,即再把网络队列实体化成可以使用的本地队列。
(6)创建任务到“本地”队列中,自动上传任务到网络队列中,分配给任务进程进行处理。

实现一【服务端】

multiprocessing_2_test.py

from multiprocessing import Process, Queue
from multiprocessing.managers import BaseManager
import task_module

# 定义任务队列和结果队列
task_queue = Queue()
result_queue = Queue()  

# 定义任务管理器类
class QueueManager(BaseManager):
    pass

# 定义任务和结果队列的获取方法
def get_task_queue():
    return task_queue

def get_result_queue():
    return result_queue

if __name__ == '__main__':
    # 注册任务队列和结果队列到网络上
    # QueueManager.register('get_task_queue', callable=lambda: task_queue)
    # QueueManager.register('get_result_queue', callable=lambda: result_queue)
    QueueManager.register('get_task_queue', callable=get_task_queue)
    QueueManager.register('get_result_queue', callable=get_result_queue)

    # 创建任务管理器实例,并绑定端口和验证口令
    # 绑定端口 5000,设置验证口令'abc'
    manager = QueueManager(address=('127.0.0.1', 5000), authkey=b'abc')

    # 启动任务管理器,监听信息通道
    manager.start()

    # 获取网络上的任务队列和结果队列
    task_queue = manager.get_task_queue()
    result_queue = manager.get_result_queue()

    # 创建任务进程
    task_process = Process(target=task_module.task_process())
    task_process.start()

    # 向任务队列中添加任务
    tasks = [1, 2, 3, 4, 5]
    for task in tasks:
        task_queue.put(task)

    # 获取任务结果并输出
    results = []
    for _ in tasks:
        result = result_queue.get(timeout=10)
        results.append(result)

    print(results)

    # 停止任务进程和任务管理器
    task_process.terminate()
    manager.shutdown()

task_module.py

import multiprocessing_2_test

# 定义任务进程函数
def task_process():
    # 从任务队列中获取任务并执行
    while True:
        task = multiprocessing_2_test.task_queue.get()
        # 执行任务...
        result = task * 2
        # 将任务结果放入结果队列中
        multiprocessing_2_test.result_queue.put(result)

【分两个文件的原因】

        在 Windows 系统上使用 multiprocessing 模块时,会遇到一个特定的限制:无法直接序列化(pickle)函数。在 Python 中,multiprocessing 模块通过序列化对象来在不同的进程之间传递数据,这包括将函数序列化以便在另一个进程中执行。然而,在 Windows 上,由于操作系统限制,无法直接序列化函数。

        因此,当你尝试启动一个新的进程并传递函数作为目标时,Python 会尝试将函数序列化以传递给新的进程,但这会导致 PicklingError,因为在 Windows 上无法序列化函数。

        为了解决这个问题,一种常见的做法是将函数移动到一个单独的模块中,然后在主程序中引入该模块并传递函数名作为参数,而不是直接传递函数对象。这样可以避免直接序列化函数对象,从而绕开了 Windows 上无法序列化函数的限制。

实现二【任务端】

任务端操作
(1)使用QueueManager注册用于获取Queue的方法名称,任务进程只能通过名称来在网络上获取Queue。
(2)连接服务器,端口和验证口令注意保持与服务进程中完全一致。
(3)从网络上获取Queue,进行本地化。
(4)从task队列获取任务,并把结果写入result队列。
# task_process.py

from multiprocessing.managers import BaseManager

# 使用 QueueManager 注册获取队列的方法
class QueueManager(BaseManager):
    pass

# 使用QueueManager注册用于获取Queue的方法名称
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')

# 连接服务器,端口和验证口令保持与服务进程中一致
server_address = ('127.0.0.1', 5000)
authkey = b'abc'

# 连接服务器并获取队列
manager = QueueManager(address=server_address, authkey=authkey)
manager.connect()

# 获取Queue的对象
task_queue = manager.get_task_queue()
result_queue = manager.get_result_queue()

# 从task队列获取任务,并把结果写入result队列
while not task_queue.empty() :
    task = task_queue.get()
    # 处理任务逻辑...
    result = "Result of processing task {}".format(task)
    result_queue.put(result)

你可能感兴趣的:(python学习,python,学习)