进程池和线程池

一、进程池

1、进程池 ProcessPoolExecutor

优点:

  1. 减少进程创建和销毁的开销: 创建和销毁进程是一个相对耗时的操作,涉及到操作系统的系统调用和资源分配。使用进程池,可以预先创建一组进程,并在需要时重用这些进程,避免了频繁的进程创建和销毁开销,提高了程序的性能和效率。

  2. 控制并发进程数量: 进程池允许你限制并发执行的进程数量,避免进程数量过多导致系统资源耗尽。通过设置进程池的大小,可以根据系统的处理能力和任务的特性来合理分配进程资源,确保进程的数量在适当范围内。

  3. 充分利用多核 CPU: 进程池可以充分利用多核 CPU 的优势,通过并行执行多个进程,提高了系统的处理能力和任务的执行效率。每个进程都拥有自己的 Python 解释器和 GIL(对于 CPython 解释器),实现了真正的并行执行,适用于 CPU 密集型任务。

  4. 避免进程资源竞争和冲突: 在多进程编程中,如果进程数量过多,可能会导致进程之间的资源竞争和冲突,如共享内存的同步问题等。使用进程池可以限制进程数量,避免过多的进程之间产生资源竞争和冲突,提高了程序的稳定性和可靠性。

  5. 提供任务队列和排队机制: 进程池通常与任务队列结合使用,可以将任务放入队列中,进程池会自动从队列中取出任务进行处理。这样可以避免任务丢失和重复执行的问题,并提供了一种简单而高效的任务调度和排队机制,使任务的处理更加有序和可控。

总的来说,进程池通过管理和重用进程、限制并发数量、提高任务调度效率等方式,优化了多进程编程的性能和资源利用,提供了一种可靠且高效的进程管理机制。在需要处理大量并发任务、充分利用多核 CPU 或涉及到共享资源的场景下,使用进程池可以有效地提升系统的性能和可扩展性。

2、使用进程池执行task任务,调用回调函数

from concurrent.futures  import ThreadPoolExecutor, ProcessPoolExecutor

def task(n, m):
    return n + m  # 1+2

def task1():
    # ...
    return {
        'username': 'kevin',
        'password': 123
    }

def callback(res):
    print(res.result())  # 固定写法

def callback1(res):
    print(res.result())
    print(res.result()['username'])

if __name__ == '__main__':
    """开一个进程池,提前放进去多个进程"""
    # max_workers 参数设置存放的进程数
    pool = ProcessPoolExecutor(2)  # 池子里面有2个工作者,其实就是实例化了5个进程

    # 有了进程池,我们现在往池子里面丢任务
    # pool.submit(task, 1, 2)
    # 进程池要主动调一个回调函数,来把结果给到我们,回调函数需要我们自己提前写好
    pool.submit(task, n=1, m=2).add_done_callback(callback)
    pool.submit(task1).add_done_callback(callback1)
    pool.shutdown()  # join + close
    print(123)

二、线程池

1、线程池是一种用于管理和重用线程的技术,优点如下:

  1. 减少线程创建和销毁的开销: 创建和销毁线程是一个相对昂贵的操作,涉及到操作系统的系统调用和资源分配。使用线程池,可以预先创建一组线程,并在需要时重用这些线程,避免了频繁的线程创建和销毁开销,提高了程序的性能和效率。

  2. 控制并发线程数量: 线程池允许你限制并发执行的线程数量,避免线程数量过多导致系统资源耗尽。通过设置线程池的大小,可以根据系统的处理能力和任务的特性来合理分配线程资源,确保线程的数量在适当范围内。

  3. 提高响应性和吞吐量: 线程池可以有效地管理和调度线程,通过并发执行多个任务,提高了系统的响应性和任务的处理吞吐量。当一个任务完成后,线程池可以立即将下一个任务分配给空闲的线程,减少了任务等待时间,提高了任务的执行效率。

  4. 避免线程资源竞争和冲突: 在多线程编程中,如果线程数量过多,可能会导致线程之间的资源竞争和冲突,如锁竞争、死锁等问题。使用线程池可以限制线程数量,避免过多的线程之间产生资源竞争和冲突,提高了程序的稳定性和可靠性。

  5. 提供任务队列和排队机制: 线程池通常与任务队列结合使用,可以将任务放入队列中,线程池会自动从队列中取出任务进行处理。这样可以避免任务丢失和重复执行的问题,并提供了一种简单而高效的任务调度和排队机制,使任务的处理更加有序和可控。

总的来说,线程池通过管理和重用线程、限制并发数量、提高任务调度效率等方式,优化了多线程编程的性能和资源利用,提供了一种可靠且高效的线程管理机制。在需要处理大量并发任务的场景下,使用线程池可以有效地提升系统的性能和可扩展性。

2、线程池 ThreadPoolExecutor

from concurrent.futures  import  ThreadPoolExecutor

def task(n, m):
    return n + m  # 1+2

def task1():
    # ...
    return {
        'username': 'kevin',
        'password': 123
    }

def callback(res):
    print(res.result())  # 固定写法

def callback1(res):
    print(res.result())
    print(res.result()['username'])

if __name__ == '__main__':
    """开一个进程池,提前放进去多个进程"""
    # max_workers 参数设置存放的进程数
    pool = ThreadPoolExecutor(2)  # 池子里面有2个工作者,其实就是实例化了2个线程

    # 有了进程池,我们现在往池子里面丢任务
    # pool.submit(task, 1, 2)
    # 进程池要主动调一个回调函数,来把结果给到我们,回调函数需要我们自己提前写好
    pool.submit(task, n=1, m=2).add_done_callback(callback)
    pool.submit(task1).add_done_callback(callback1)
    pool.shutdown()  # join + close
    print(123)

3、使用线程池爬取网页

这里爬取到的网页内容是二进制的,所以写入用wb模式

import requests
from  concurrent.futures  import  ThreadPoolExecutor, ProcessPoolExecutor

def get_page(url):
    res = requests.get(url)  # 爬取网页
    name = url.rsplit('/')[-1] + '.html'
    return  {'name': name, 'text': res.content}

# 使用回调函数时会返回一个 concurrent.futures.Future 对象,
# 它代表了异步操作的结果。res.result() 是 Future 对象的方法,用于获取异步操作的结果.
def call_back(res):
    print(res.result()['name'])   # 回调函数获取的是task的返回值,这里get_page返回值是一个字典
    with open(res.result()['name'], 'wb') as f:
        f.write(res.result()['text'])

if __name__ == '__main__':
    pool = ThreadPoolExecutor(2)
    urls = ['http://www.baidu.com', 'http://www.cnblogs.com', 'http://www.taobao.com']
    for url in urls:
        pool.submit(get_page, url).add_done_callback(call_back)

# www.baidu.com.html
# www.cnblogs.com.html
# www.taobao.com.html

三、进程池的两种实现机制

multiprocessing.Pool 和 concurrent.futures.ProcessPoolExecutor

1、multiprocessing.Pool的优点和适用场景:

  • 优点:
    • 使用Python标准库中的multiprocessing模块,易于使用和理解。
    • 提供了方便的方法,如apply_async用于异步提交任务,closejoin用于管理进程池。
    • 可以根据并发需求灵活设置进程池的大小。
  • 缺点:
    • 在某些情况下,由于进程间通信的开销和资源消耗,使用多进程可能会导致更高的系统开销。
    • 在Windows系统中,multiprocessing.Pool使用的是fork方式,而不是spawn方式,可能会受到一些限制。
  • 适用场景:
    • 一般的并发连接处理。
    • 对于计算密集型任务,其中存在大量CPU计算,并且不涉及过多的进程间通信。

2、concurrent.futures.ProcessPoolExecutor的优点和适用场景:

  • 优点:
    • 基于concurrent.futures模块,提供了高级的异步执行和进程池的接口。
    • 可以更方便地使用上下文管理器(with语句)来管理进程池的生命周期。
    • 具有与ThreadPoolExecutor相同的接口,使得在需要切换到线程池的情况下更加方便。
  • 缺点:
    • 在某些情况下,由于进程间通信的开销和资源消耗,使用多进程可能会导致更高的系统开销。
    • 在Windows系统中,仍然受到一些限制,因为它仍然使用的是multiprocessing.Pool
  • 适用场景:
    • 对于需要更高级和灵活接口的进程池需求。
    • 可以与concurrent.futures.ThreadPoolExecutor一起使用,在不同场景之间进行切换。

四、结合socket编程实现并发接收客户端

服务端方式一: ProcessPoolExecutor

import socket
from concurrent.futures import ProcessPoolExecutor

def handle_client(sock):  # handle:处理,处理客户端的收发信息
    while True:
        try:
            data = sock.recv(1024)   # 不停的收
            print('客户端发来的消息是:%s' % data)
            if len(data) == 0:
                break
            sock.send(b'this is server!')  # 不停的发
        except ConnectionResetError as e:
            print(e)
            break

    sock.close()  # 关闭客户端

if __name__ == '__main__':
    server = socket.socket()   # 实例化server
    server.bind(('127.0.0.1', 8001))  # 绑定
    server.listen(4)  # 监听

    with ProcessPoolExecutor(5) as executor:  # 创建进程池,最大进程数为5
        while True:
            sock, addr = server.accept()
            executor.submit(handle_client, sock)  # 使用进程池提交任务

    server.close()


#####不用with的方式如下:
'''
    pool = ProcessPoolExecutor(5)
    while True:
        sock, addr = server.accept()
        pool.submit(handle_client, sock)

    pool.shutdown()  # join + close
    server.close()
'''

注⚠️:

with语句用于创建一个上下文管理器,确保进程池正确地被创建和关闭,以及释放相应的系统资源。

ProcessPoolExecutor(5)创建了一个进程池,最大进程数为5。使用with语句可以确保在代码块结束后,进程池资源会被正确地关闭和释放

进程池中的任务是通过executor.submit(handle_client, sock)提交的。每当有一个客户端连接时,handle_client函数会被提交到进程池中进行处理。这样,可以在同一时间处理多个客户端连接,利用多进程的并行能力提高处理效率。

使用with ProcessPoolExecutor(5) as executor的好处是,无论代码块中发生什么异常或错误,进程池都会被正确地关闭,以防止资源泄漏或未被释放的情况发生。这是由ProcessPoolExecutor类实现的上下文管理器协议所提供的功能。

服务端方式二: 

import socket
import multiprocessing as mp

def handle_client(sock):
    while True:
        try:
            data = sock.recv(1024)
            print('客户端发来的消息是:%s' % data)
            if len(data) == 0:
                break
            sock.send(b'this is server!')
        except ConnectionResetError as e:
            print(e)
            break

    sock.close()

if __name__ == '__main__':
    server = socket.socket()
    server.bind(('127.0.0.1', 8001))
    server.listen(4)

    pool = mp.Pool(processes=5)  # 创建进程池,指定进程数为5

    while True:
        sock, addr = server.accept()
        pool.apply_async(handle_client, args=(sock,))  # 使用进程池异步提交任务

    pool.close()
    pool.join()
    server.close()

客户端

import socket

# 1. 实例化对象
client = socket.socket()  # 默认的参数就是基于网络的tcp socket

# 2. 连接服务端
client.connect(('127.0.0.1', 8001))

while True:
    res = input('请输入你要发送的消息:')

    if res == 'exit':
        break

    if res == '':
        continue

    # 3. 给服务端发送消息
    client.send(res.encode('utf8'))

    # 4. 接受服务端发来的消息
    data = client.recv(1024)
    print('服务端发来的消息:%s' % data)

# 5. 断开连接
client.close()

  

 

posted @ 2023-07-07 20:53  凡人半睁眼  阅读(79)  评论(0编辑  收藏  举报