Python实现线程池工作模式的案例详解

目录
  • 01、客户机/服务器通信逻辑
  • 02、数据交换协议
  • 03、服务器主体逻辑
  • 04、服务器会话线程
  • 05、客户机主体逻辑
  • 06、客户机发送数据
  • 07、客户机接收数据
  • 08、客户机界面设计
  • 09、线程池
  • 10、联合测试
  • 11、小结

本文章基于苹果树病虫害预测模型,自定义应用层通信逻辑,设计服务器与客户机。客户机向服务器发送图像数据,服务器回送预测结果。为增强服务器的可靠性与可扩展性,服务器端采用线程池工作模式。为了增强客户机的可操作性,客户机采用PyQt5完成图形化界面设计。

01、客户机/服务器通信逻辑

客户机与服务器通信逻辑如图1所示。

■图1 智能桌面App的客户机/服务器通信逻辑

02、数据交换协议

客户机与服务器之间一次信息往返的协议会话过程,定义为图2所示的逻辑时序。

■ 图2 应用层通信协议

协议会话逻辑解析:

(1)消息交换基于消息头机制。消息头中包含消息类型和消息长度。消息类型包含图像消息与下线消息。

(2)用Json格式的数据表示消息头。图像数据用base64编码与解码。

(3)发送数据分两个步骤完成,首先发送消息头,然后发送消息内容。

(4)接收数据分两个步骤完成,首先接收消息头,然后接收消息内容。

消息头的结构设计如图3所示,消息头的固定长度为128字节,包含消息类型(msg_type)与消息内容长度(msg_len)两个字段。

■图3 消息头的结构

消息类型包括:

(1)CLIENT_IMAGE:表示收到来自客户机的图像数据。

(2)CLIENT_MESSAGE:表示收到来自客户机的下线消息。

消息内容长度用消息包含的字符数表示。对于图像数据而言,因为采用base64编码,其传输的数据也是字符消息。

消息头的长度在服务器与客户机两端均约定为128字节,用常量MSG_HEADER_LEN定义。发送消息头之前,需要检查消息的长度,如果不足128字节,其左侧用字节型空格字符填充。

03、服务器主体逻辑

根据图1描述的服务器逻辑,完成服务器的主体逻辑设计,如程序段P7.1所示。

第32~39行定义服务器端的主循环,处理客户机连接,采用的是一客户一线程模式。服务器会话线程定义为handle_client模块,主线程向会话线程传递三个参数:

(1)client_socket: 会话套接字

(2)client_addr: 客户机地址

(3)model: 用于预测的智能模型

运行服务器程序,观察输出结果,此时服务器虽然处于侦听连接的状态,但是由于handle_client模块还没有实现,故无法处理来自客户机的各种消息。

04、服务器会话线程

服务器会话线程包括接收数据与发送数据两个模块,对应图1中的内循环。服务器完成数据接收后,需要回送预测结果或者确认消息给客户机,所以将接收数据与发送数据的逻辑定义在同一函数模块handle_client中,收发数据的逻辑流程如图4所示。

■图4 服务器收发数据会话线程逻辑

会话线程的主逻辑是一个循环,循环条件为远程客户机是否结束会话,逻辑流程解析如下:

(1)如果客户机断开了与服务器的连接,会话线程结束。

(2)在连接正常的情况下,服务器首先接收来自客户机的消息头,解析消息头,根据消息类型,分为一般消息与图像消息。

(3)如果是图像消息,则通过一个循环,根据图像的大小完成数据接收,然后经过base64解码、图像变换(调整颜色模式、归一化、缩放)、模型预测、重构预测结果、定义消息头、回送消息头、回送预测结果。回到步骤(1)。

(4)如果是一般消息,则继续判断是否为下线消息。

(5)如果是下线消息,则更新连接数量,定义下线消息(原消息加上时间戳),定义消息头,回送消息头,回送消息内容,会话线程结束。

(6)如果不是下线消息,则做其他消息处理,为简化设计,其他消息处理模块暂不编程,留作扩展。回到步骤(1)。

会话线程handle_client的逻辑实现如程序段P7.2所示。

第46行–第51行定义的循环结构,根据图像数据的长度msg_len完成数据接收工作。

运行服务器程序,输出结果为:

服务器开始在('192.168.0.102', 5050)侦听...

待客户机程序完成后,再做联合测试。

05、客户机主体逻辑

新建主程序MyClient.py。根据图1描述的客户机逻辑,完成客户机的主体逻辑设计,其主要模块如图5所示。

模块send_image_data发送图像数据,模块send_down_msg发送下线消息,模块recv_message是用于接收服务器消息的会话线程,类模块GUI(QMainWindow)负责构建客户机图形化界面。主程序完成主控逻辑设计。

■图5 客户机程序模块结构

客户机的消息结构定义如图3所示,与服务器保持一致。消息的收发逻辑,如图2所示,亦与服务器保持一致。

客户机主体逻辑如程序段P7.3所示。

首先运行服务器程序,然后运行测试客户机程序。目前客户机还做不了具体工作,输入字符Q退出客户机主循环。

06、客户机发送数据

客户机向服务器发送的数据有两种类型,一是图像数据,一是下线消息。发送图像数据的流程如图6所示。

■图6 发送图像数据流程

程序段P7.4描述了发送图像数据模块send_image_data的完整逻辑。

07、客户机接收数据

客户机定义了线程函数recv_message,用于接收两类数据,一是普通消息(下线消息等),二是预测消息(预测结果)。消息处理流程如图7所示,分步描述如下。

(1)进入消息循环,接收消息头。

(2)如果消息头为空,转到步骤(1)。

(3)如果消息头非空,则解析消息头,获取消息类型与消息长度。

(4)如果是普通消息,则接收消息内容,进一步判断是否为下线消息。

(5)如果是下线消息,则跳出消息循环,转到步骤(9)。

(6)如果非下线消息,则转到步骤(1)。

(7)如果不是普通消息,则判断是否为预测消息,如果不是预测消息,则转到步骤(1)。

(8)如果是预测消息,则接收消息内容,解析消息内容,将预测结果存入队列中,显示预测结果。转到步骤(1)。

(9)显示下线消息,消息接收线程结束。

■图7 客户机接收消息逻辑流程

程序段P7.6描述了接收消息线程函数recv_message的完整逻辑。

将\dataset\images目录下的图像文件Test_0.jpg、Test_7.jpg拷贝到根目录下。

运行服务器程序,然后运行客户机程序,做联合测试。

客户机输入待遇测的图像文件名称Test_0.jpg,回车后发送图像数据,服务器返回预测结果。客户机输入字符Q,结束客户机。完成此次客户机与服务器的通信后,服务器与客户机的状态信息如图8所示。

■图8 客户机与服务器联合测试

此时服务器工作于一客户一线程模式,启动多个客户端,可做联合测试。

08、客户机界面设计

为了增强客户机的可操作性,基于PyQt5框架为客户机设计图形化界面,界面布局及其控件名称如图9所示。

■图9 客户机图形化界面布局

定义图形化界面类GUI(QMainWindow)封装图9所示的控件及其事件函数。

运行服务器,然后运行客户机,从chapter7的根目录中加载图像Test_0.jpg,观察图像特点。然后单击“预测”按钮,观察服务器反馈的预测结果,如图10所示。

■图10 客户机图形化界面测试结果

09、线程池

服务器现有的工作模式为一客户一线程,即为每一个连接到服务器的客户机创建独立的会话线程,当客户机并发量较大时,服务器往往面临资源枯竭的挑战。

线程池模式可以有效平衡服务器负载能力,与一客户一线程模式相比,其主要优点有:

(1)通过重用已存在的线程,降低线程创建和销毁造成的额外消耗。

(2)提高系统响应速度,当有新任务到达时,通过复用已存在的线程便能立即执行,无需等待新线程的创建。

(3)控制资源消耗,将并发线程数量限制在合理的区间。

(4)针对工作线程提供了更多的控制能力,例如线程延时、定时等。

Python的线程池定义在concurrent.futures包中,使用ThreadPoolExecutor类创建线程池。线程池调度任务过程如图11所示。

■图11 线程池调度任务示意图

将一客户一线程模式修改为线程池模式,只需做以下改动:

(1)导入线程池类ThreadPoolExecutor。在服务器端添加语句:

from concurrent.futures import ThreadPoolExecutor # 线程池类

(2)在服务器主线程的while循环前面添加创建线程池的语句:

pool = ThreadPoolExecutor(max_workers=5) # 创建线程池,指定工作线程数量为5

此处如果省略参数max_workers,则线程池默认工作线程数量是CPU数量的5倍。考虑到线程池往往应用于需要大量I/O交换的场景,而不是CPU计算密集型的场景,故工作线程的数量应该超过CPU的数量。

(3)用线程池调度语句替换原有的线程创建语句。

# 建立与客户机会话的线程,一客户一线程
client_thread = threading.Thread(target=handle_client, args=(new_socket, new_addr, model))
client_thread.start()

替换为:

pool.submit(handle_client,new_socket, new_addr, model) # 创建线程任务,提交到线程池

(4)在主程序末尾,while循环外部,添加关闭线程池的语句,释放资源:

pool.shutdown(wait=True) # 关闭线程池

执行shutdown后,线程池将不再接受新任务。参数wait默认为True,表示关闭线程池之前需要等待所有工作线程结束。

10、联合测试

为便于观察,将服务器线程池的工作线程数量调整为2。启动服务器,然后启动四个客户机,标识为客户机1、客户机2、客户机3、客户机4。

四个客户机从dataset\images目录中选择四幅不同的测试图片,

假定客户机1选择的图片是Test_17.jpg,客户机2选择的是Test_152.jpg,客户机3选择的是Test_190.jpg,客户机4选择的是Test_1572.jpg,然后依次点击客户机1、客户机2、客户机3、客户机4的“预测”按钮,观察预测结果。

可以看到,只有客户机1、客户机2立即反馈了预测结果,而客户机3、客户机4虽然已经连接到服务器,却并没有立即得到预测结果,原因是服务器线程池大小为2,客户机3、客户机4需要在任务队列等待。

客户机1显示结果如图12所示。

■图12 客户机1的预测结果

客户机2显示结果图13所示。

■图13 客户机2的预测结果

客户机3显示结果如图14所示。由于服务器线程池大小为2,所以客户机1与客户机2占用工作线程后,客户机3只能进入任务队列等待。

■图14 客户机3处于等待中

客户机4显示结果如图15所示。同样,客户机4也只能进入服务器的任务队列等待。

■图15 客户机4处于等待中

关闭客户机1,则会自动释放客户机1占用的工作线程,此时排队中的客户机3会立即得到相应,其结果如图16所示。

■图16 客户机3得到服务器响应

此时只有客户机4仍处于等待中。如果继续关闭客户机2,则客户机4会得到立即响应,其预测结果如图17所示。

■图17 客户机4得到服务器响应

关闭客户机3、关闭客户机4。整个会话期间,服务器状态监控界面的信息提示如下:

仔细阅读服务器的状态提示信息,与客户机的操作相对照,可以更精准地把握客户机与服务器的全程会话逻辑。

11、小结

本文基于Socket通信方法,自定义数据交换协议,围绕苹果树病虫害识别需求,迭代构建了客户机/服务器模式的智能桌面App。图像数据的发送采用base64编码方式,消息头、消息内容采用Json数据格式。服务器端采用一客户一线程和线程池技术支持并发访问,客户机采用基于PyQt5的图像化界面技术提高其可操作性。基于Socket技术的网络编程,在客户机与服务器两端提供了更多的设计灵活性。

到此这篇关于Python实现线程池工作模式的文章就介绍到这了,更多相关Python线程池工作模式内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • python操作RabbitMq的三种工作模式

    一.简介: RabbitMq 是实现了高级消息队列协议(AMQP)的开源消息代理中间件.消息队列是一种应用程序对应用程序的通行方式,应用程序通过写消息,将消息传递于队列,由另一应用程序读取 完成通信.而作为中间件的 RabbitMq 无疑是目前最流行的消息队列之一. ​ RabbitMq 应用场景广泛: 系统的高可用:日常生活当中各种商城秒杀,高流量,高并发的场景.当服务器接收到如此大量请求处理业务时,有宕机的风险.某些业务可能极其复杂,但这部分不是高时效性,不需要立即反馈给用户,我们可以将这部

  • 详解Python 实现 ZeroMQ 的三种基本工作模式

    简介 引用官方说法:ZMQ(以下 ZeroMQ 简称 ZMQ)是一个简单好用的传输层,像框架一样的一个 socket library,他使得 Socket 编程更加简单.简洁和性能更高. 是一个消息处理队列库,可在多个线程.内核和主机盒之间弹性伸缩. ZMQ 的明确目标是"成为标准网络协议栈的一部分,之后进入 Linux 内核".现在还未看到它们的成功.但是,它无疑是极具前景的.并且是人们更加需要的"传统" BSD 套接字之上的一 层封装.ZMQ 让编写高性能网络应

  • Python实现线程池工作模式的案例详解

    目录 01.客户机/服务器通信逻辑 02.数据交换协议 03.服务器主体逻辑 04.服务器会话线程 05.客户机主体逻辑 06.客户机发送数据 07.客户机接收数据 08.客户机界面设计 09.线程池 10.联合测试 11.小结 本文章基于苹果树病虫害预测模型,自定义应用层通信逻辑,设计服务器与客户机.客户机向服务器发送图像数据,服务器回送预测结果.为增强服务器的可靠性与可扩展性,服务器端采用线程池工作模式.为了增强客户机的可操作性,客户机采用PyQt5完成图形化界面设计. 01.客户机/服务器

  • Python学习之线程池与GIL全局锁详解

    目录 线程池 线程池的创建 - concurrent 线程池的常用方法 线程池演示案例 线程锁 利用线程池实现抽奖小案例 GIL全局锁 GIL 的作用 线程池 线程池的创建 - concurrent concurrent 是 Python 的内置包,使用它可以帮助我们完成创建线程池的任务. 方法名 介绍 示例 futures.ThreadPoolExecutor 创建线程池 tpool=ThreadPoolExecutor(max_workers) 通过调用 concurrent 包的 futu

  • python多进程使用及线程池的使用方法代码详解

    多进程:主要运行multiprocessing模块 import os,time import sys from multiprocessing import Process class MyProcess(Process): """docstring for MyProcess""" def __init__(self, arg, callback): super(MyProcess, self).__init__() self.arg = a

  • ThreadPoolExecutor线程池原理及其execute方法(详解)

    jdk1.7.0_79 对于线程池大部分人可能会用,也知道为什么用.无非就是任务需要异步执行,再者就是线程需要统一管理起来.对于从线程池中获取线程,大部分人可能只知道,我现在需要一个线程来执行一个任务,那我就把任务丢到线程池里,线程池里有空闲的线程就执行,没有空闲的线程就等待.实际上对于线程池的执行原理远远不止这么简单. 在Java并发包中提供了线程池类--ThreadPoolExecutor,实际上更多的我们可能用到的是Executors工厂类为我们提供的线程池:newFixedThreadP

  • java设计模式责任链模式原理案例详解

    目录 引言 责任链模式定义 类图 角色 核心 示例代码 1.对请求处理者的抽象 2.对请求处理者的抽象 3.责任链的创建 责任链实现请假案例 案例类图 可扩展性 纯与不纯的责任链模式 纯的责任链模式 不纯的责任链模式 责任链模式主要优点 职责链模式的主要缺点 适用场景 模拟实现Tomcat中的过滤器机制 运行过程如下 分析Tomcat 过滤器中的责任链模式 引言 以请假流程为例,一般公司普通员工的请假流程简化如下: 普通员工发起一个请假申请,当请假天数小于3天时只需要得到主管批准即可:当请假天数

  • Android 启动模式FLAG_ACTIVITY_CLEAR_TOP案例详解

    四种启动模式 standard: 只要被启动就会创建一个新的 singleTop: 栈顶复用(当被启动的Activity处于Task栈顶时,可以复用,直接调用onNewIntent方法) singleTask: 栈中复用(被启动的Activity已经处于栈中,会将上边的Activity清除出栈,调用onNewIntent) singleInstance 全局单实例(应用场景:地图,Activity初始化需要大量资源) Intent的标志位FLAG Intent.FLAG_ACTIVITY_SIN

  • Python 经典贪心算法之Prim算法案例详解

    最小生成树的Prim算法也是贪心算法的一大经典应用.Prim算法的特点是时刻维护一棵树,算法不断加边,加的过程始终是一棵树. Prim算法过程: 一条边一条边地加, 维护一棵树. 初始 E = {}空集合, V = {任选的一个起始节点} 循环(n – 1)次,每次选择一条边(v1,v2), 满足:v1属于V , v2不属于V.且(v1,v2)权值最小. E = E + (v1,v2) V = V + v2 最终E中的边是一棵最小生成树, V包含了全部节点. 以下图为例介绍Prim算法的执行过程

  • Docker工作模式及原理详解

    如下图所示: 我们在使用虚拟机和docker的时候,就会出现这样一个疑问:Docker为什么比VM虚拟机快呢? 上面这张图就很客观的说明了这个问题 1.Docker有着比虚拟机更少的抽象层. 2.Docker利用的是宿主机的内核,VM需要的是Guest os. 所以说,新建一个容器的时候,docker不需要像虚拟机一样重新加载一个操作系统.虚拟机是加载Guest os(花费时间分钟级别),而docker利用的是宿主机的操作系统,省略了这个复杂的过程(花费时间秒级别). 搞清楚这些,我们再来看看对

  • Java线程池队列PriorityBlockingQueue和SynchronousQueue详解

    目录 正文 PriorityBlockingQueue阻塞优先队列 SynchronousQueue 正文 public enum QueueTypeEnum { ARRAY_BLOCKING_QUEUE(1, "ArrayBlockingQueue"), LINKED_BLOCKING_QUEUE(2, "LinkedBlockingQueue"), DELAY_QUEUE(3, "DelayQueue"), PRIORITY_BLOCKING

  • Java线程池的拒绝策略实现详解

    一.简介 jdk1.5 版本新增了JUC并发编程包,大大的简化了传统的多线程开发. Java线程池,是典型的池化思想的产物,类似的还有数据库的连接池.redis的连接池等.池化思想,就是在初始的时候去申请资源,创建一批可使用的连接,这样在使用的时候,就不必再进行创建连接信息的开销了.举个生活中鲜明的例子,在去著名洋快餐某基或者某劳的时候,配餐人员是字节从一个中间的保温箱里面直接取,然后打包就好了.不用再临时的来了一个单子,又要去拿原材料,又要去进行加工.效率明显的就是提高了很多. 既然是池子,那

随机推荐