Python多线程 threading 和多进程 multiprocessing

1. 并发 vs 并行

线程是程序执行的最小单位,一个进程可以由一个或多个线程组成,各个线程之间也是交叉执行。

  • 并发,相当于单核CPU宏观同时执行,微观高速切换 交替执行。多线程、高并发这些词语更多地出现在服务端程序里。

  • 并行,相当于多核CPU微观同时执行,更强调提升性能上限。多进程更多地与高性能计算、分布式计算联系在一起。

  • 多进程:同时运行多个独立的进程,每个进程有自己独立的内存空间和执行上下文,彼此之间相互独立。

  • 多线程:同一进程中同时运行多个线程,线程是进程的一部分,共享同一个进程的内存空间和执行上下文。

多进/线程 与 顺序串行执行的区别
多进程和多线程都可以实现并行和并发的效果,但也可以减少任务间的等待时间,提高效率,但相较于顺序串行执行,进程/线程的切换也会引起时间损耗

多进程 与 多线程的区别

  • 切换速度:多个线程共享同一个进程的内存空间和执行上下文,线程之间的切换比进程切换更快速,开销相对较小。
  • 安全性:由于进程之间相互独立,因此多进程编程更安全,而多线程编程需要更加仔细地管理线程之间的同步和互斥,小心地处理共享数据,防止出现竞态条件等并发问题。

目前电脑主流配置都是四核-八线程的,而实际工作的任务数大都大于四个,所以也是需要交替来并发执行具体任务的。
在这里插入图片描述

话不多说,下面举一个例子(同时计算3个不同数字序列的欧拉数),分别用顺序串行执行多线程调用多进程调用计算3个task,进行计时测速:

import threading as th
import multiprocessing as mp
import time
from functools import wrapsdef timer(func):  # 函数的通用计时器,使用时在函数前面声明@timer即可@wraps(func)def inner_func():t = time.time()rts = func()print(f"timer: using {time.time() - t :.5f} s")return rtsreturn inner_funcdef euler_func(n: int) -> int:res = ni = 2while i <= n // i:if n % i == 0:res = res // i * (i - 1)while (n % i == 0): n = n // ii += 1if n > 1:res = res // n * (n - 1)return restask1 = list(range(2, 50000, 3))  # 2, 5, ...
task2 = list(range(3, 50000, 3))  # 3, 6, ...
task3 = list(range(4, 50000, 3))  # 4, 7, ...def job(task: list):for t in task:euler_func(t)@timer
def normal():  # 顺序串行执行job(task1)job(task2)job(task3)@timer  # @timer 是上面写的修饰器
def mutlthread():  # 多线程调用th1 = th.Thread(target=job, args=(task1, ))th2 = th.Thread(target=job, args=(task2, ))th3 = th.Thread(target=job, args=(task3, ))# start() ,告诉线程/进程:你可以开始干活了,程序主逻辑还得继续往下运行th1.start()th2.start()th3.start()# 到 join() 这里,咱们是指让线程/进程阻塞住咱的主逻辑,比如p1.join()是指:p1不干完活,我主逻辑不往下进行(属于是「阻塞」)th1.join()th2.join()th3.join()@timer
def multcore():  # 多进程调用p1 = mp.Process(target=job, args=(task1, ))p2 = mp.Process(target=job, args=(task2, ))p3 = mp.Process(target=job, args=(task3, ))# start() ,告诉线程/进程:你可以开始干活了,程序主逻辑还得继续往下运行p1.start()p2.start()p3.start()# 到 join() 这里,咱们是指让线程/进程阻塞住咱的主逻辑,比如p1.join()是指:p1不干完活,我主逻辑不往下进行(属于是「阻塞」)p1.join()p2.join()p3.join()if __name__ == '__main__':print("同步串行:"); normal()print("多线程并发:"); mutlthread()print("多进程并行:"); multcore()

结果分析多线程并发的方式具有较好的性能表现。

  • 多线程并发可以在同一时间内执行多个子任务,利用了计算机多核心的优势,可以更高效地完成任务。
  • 而同步串行的方式需要等待每个子任务完成后才能进行下一个任务,造成了一定的时间延迟。
  • 多进程并行的方式虽然也能同时执行多个子任务,但由于进程之间的切换和数据通信开销较大,导致执行时间略长于多线程并发。

需要注意的是,不同的任务和计算环境可能会导致不同的结果。

同步串行:
timer: using 0.44006 s
多线程并发:
timer: using 0.30993 s
多进程并行:
timer: using 0.41000 s

2. 多任务模式

实现多任务的方式主要有以下2种:
1、多进程模式 multiprocessing
2、多线程模式 threading

同时执行多个任务通常各个任务之间并不是没有关联的,而是需要相互通信和协调,有时,任务1必须暂停等待任务2完成后才能继续执行,有时,任务3和任务4又不能同时执行,所以,多进程和多线程的程序的复杂度要远远高于我们前面写的单进程单线程的程序。

比如,算法C/S架构部署时,服务端拉rtmp视频流进行视频帧解析、算法推理、结果推流多个相互依赖的任务时,可以使用多线程、多进程的方式。

2.1 多进程模式 multiprocessing

进程是操作系统中的一个执行实体,每个进程都有自己的内存空间,彼此互不影响。一般进程数默认是电脑CPU核数,当你的电脑是四核的时候,你的电脑进程默认就是4个。

在Python中我们借助多进程包multiprocessing来进行多进程任务处理方式, multiprocessing模块提供了一个Process类来代表一个进程对象:

# Process参数解析
multiprocessing.Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
#group分组
#target表示调用对象!即此进程调用的函数名
#name表示进程的别名
#args表示调用对象的位置参数元组,即函数的参数
#kwargs表示调用对象的字典# Process类的常用方法
close() 关闭进程
is_alive() 进程是否在运行
join() 等待join语句之前的所有程序执行完毕以后再继续往下运行,通常用于进程间的同步
start()  进程准备就绪,等待CPU调度
run()  strat()调用run方法,如果实例化进程时没有传入target参数,这star执行默认run()方法# Process类的常用属性
pid 进程ID
name 进程名字

下面的例子演示了启动一个子进程(即单进程),并等待其结束:一个子进程其实就和我们平常调用单一函数是一样的。

from multiprocessing import Process
import os# 子进程要执行的代码
def run_proc(name):print('Run child process %s (%s)...' % (name, os.getpid()))if __name__=='__main__':print('Parent process %s.' % os.getpid())#用来获取 主进程 的进程IDp = Process(target=run_proc, args=('test',))#实例化进程p,调用run_proc函数,传入参数对象argsprint('Child process will start.')p.start()#p进程准备就绪p.join()#调用p进程,主进程等待p进程执行print('Child process end.')

建立多个子进程(即多进程),其实就是多个函数随机同步运行,建立多进程有两种方法,一种是直接实例化多个进程对象,另一种是进程池(Pool)

  1. 直接利用multiprocessing.Process()来实例化多个子进程调用不同的函数即可。但当任务数(需要调用的函数)较多时,就需要实例化多个进程对象,并且写多行p.start()来就绪比较麻烦,如下:
from multiprocessing import Process
import random,timedef do_task(task):print('我正在做{}'.format(task))time.sleep(random.randint(1,3))def write_task(task):print('我正在写{}'.format(task))time.sleep(random.randint(1,3))if __name__ == "__main__":p1 = Process(target=do_task,args=('PPT',))p2 = Process(target=write_task,args=('Sql',))p1.start()p2.start()
  1. 使用multiprocessing.Pool(processing_number)实例化一个包含processing_number个进程的进程池,使用apply_async()方法来异步地提交任务给进程池进行处理。
multiprocessing.Pool = Pool(processes_number: int)#process为进程数

把上面的例子用进程池Pool表示以后的结果如下:

import multiprocessing
import random
import time
from multiprocessing import Process, Queuedef do_task(q, task):print('我正在做{}'.format(task))q.put(task)  # 将消息入队尾time.sleep(random.randint(1, 3))def listen_task(q, xxx):if (not q.empty()):print('我收到了,你做完了{}'.format(q.get()))  # 从队头取出消息else:print('queue empty')time.sleep(random.randint(1, 3))if __name__ == "__main__":q = Queue() # 注意进程通信要用multiprocessing.Queuep1 = Process(target=do_task,args=(q, ['PPT',]))p2 = Process(target=listen_task,args=(q, ['Sql',]))p1.start()p2.start()p1.join()p2.join()print('All subprocesses done.')

输出结果如下:

Waiting for all subprocesses done...
我正在做PPT
我正在写Sql
All subprocesses done.

进程通信:由于进程不共享内存,因此进程间通信(IPC)需要使用特定的机制,如管道(Pipe)队列(Queue)等。标准库的Queue只能实现线程间的通信,其get,put方法是阻塞的!!Queue.Queue是进程内非阻塞队列,multiprocess.Queue是跨进程通信队列。多进程前者是各自私有,后者是各子进程共有。

from multiprocessing import Process, Queuedef worker(q):q.put('Hello from process') # 字符串消息入队q.put()if __name__ == '__main__':q = Queue() # 实例化队列qprocess = Process(target=worker, args=(q,))process.start()process.join()print(q.get())  # 从队头中取出消息q.get()

特别说明:MultiProcessing.Process创建的进程是有共同父进程的,而MultiProcess.Pool创建的进程则不是。

MultiProcessing.Process创建的进程能够使用MultiProcessing的Queue通信,但是如果使用的是进程池创建的进程,那么就得使用Manager类封装的数据结构了。

Queue.qsize() 返回队列的大小  
Queue.empty() 如果队列为空,返回True,反之False  
Queue.full() 如果队列满了,返回True,反之False 
Queue.get([block[, timeout]]) 获取队列,timeout等待时间  
Queue.get_nowait() 相当Queue.get(False) 
非阻塞 Queue.put(item) 写入队列,timeout等待时间  
Queue.put_nowait(item) 相当Queue.put(item, False)

2.2 多线程模式 threading

多线程模式就是一次只启动一个进程,但是在这个进程里启动多个线程,这样多个线程就可以一起执行多个任务,在Python中我们要启动多线程借助于threading模块中的Thread类,构建时使用的参数和方法与Process基本一致。

# Thread参数解析
Thread(group=None, target=None, name=None, args=(), kwargs={}) 
#方法
isAlive()
get/setName(name) 获取/设置线程名
start()   
join() 

创建一个线程就是调用一个函数:

import time, threadingdef do_chioce(task):print('我正在{}'.format(task))time.sleep(random.randint(1,3))if __name__ == "__main__":t = threading.Thread(target=do_chioce,args=('选PPT模板',)) # 实例化线程tt.start() # 调用t线程

创建多个线程就是调用多个函数,do_chioce()和do_content()函数都在各自的线程t1, t2中执行,彼此互不干扰:

import time, threadingdef do_chioce(task):print('我正在{}'.format(task))time.sleep(random.randint(1,3))def do_content(task):print('我正在{}'.format(task))time.sleep(random.randint(1,3))if __name__ == "__main__":t1 = threading.Thread(target=do_chioce,args=('选PPT模板',))t2 = threading.Thread(target=do_content,args=('列PPT大纲',))t1.start()t2.start()

线程通信:由于线程共享内存,因此线程间的数据是可以互相访问的。但是,当多个线程同时修改数据时就会出现问题。为了解决这个问题,我们需要使用线程同步工具,如锁(Lock)和条件(Condition)等。

import threadingclass BankAccount:def __init__(self):self.balance = 100  # 共享数据self.lock = threading.Lock()def deposit(self, amount):with self.lock:  # 使用锁进行线程同步balance = self.balancebalance += amountself.balance = balancedef withdraw(self, amount):with self.lock:  # 使用锁进行线程同步balance = self.balancebalance -= amountself.balance = balancedef deposit_money(account, amount):for _ in range(100000):account.deposit(amount)def withdraw_money(account, amount):for _ in range(100000):account.withdraw(amount)account = BankAccount()# 创建两个线程,一个存款,一个取款
deposit_thread = threading.Thread(target=deposit_money, args=(account, 10))
withdraw_thread = threading.Thread(target=withdraw_money, args=(account, 5))# 启动线程
deposit_thread.start()
withdraw_thread.start()# 等待线程结束
deposit_thread.join()
withdraw_thread.join()print(f"最终余额: {account.balance}")

在这个例子中,我们创建了一个银行账户对象account,初始余额为100。然后,我们创建了两个线程,一个用于存款,一个用于取款。每个线程都会对账户进行一定次数的操作(存款或取款)。通过使用线程锁,在进行存款或取款操作时,我们保证了对balance变量的访问是同步的,避免了数据竞争和不一致性。

特别说明:Python的线程虽然受到全局解释器锁(GIL)的限制,但是对于IO密集型任务(如网络IO或者磁盘IO),使用多线程可以显著提高程序的执行效率。

3. OpenCV 视频流的多线程方法

线程是进程中的一个执行单元。多线程是指通过在线程之间快速切换对 CPU 的控制(称为上下文切换)来并发执行多个线程。在我们的示例中,我们将看到多线程通过提高 FPS(每秒帧数)实现更快的实时视频处理。
在这里插入图片描述
原因多线程有助于更快的处理

视频处理代码分为两部分:从摄像头读取下一个可用帧并对帧进行视频处理,例如运行深度学习模型进行人脸识别等。

读取下一帧并在没有多线程的程序中按顺序进行处理。程序等待下一帧可用,然后再对其进行必要的处理。读取帧所需的时间主要与请求、等待和将下一个视频帧从相机传输到内存所需的时间有关。对视频帧进行计算所花费的时间,无论是在 CPU 还是 GPU 上,占据了视频处理所花费的大部分时间。

在具有多线程的程序中,读取下一帧并处理它不需要是顺序的。当一个线程执行读取下一帧的任务时,主线程可以使用 CPU 或 GPU 来处理最后读取的帧。这样,通过重叠两个任务,可以减少读取处理帧的总时间。

没有多线程的代码:

# importing required libraries 
import cv2 
import time# opening video capture stream
vcap = cv2.VideoCapture(0)
if vcap.isOpened() is False :print("[Exiting]: Error accessing webcam stream.")exit(0)
fps_input_stream = int(vcap.get(5))
print("FPS of webcam hardware/input stream: {}".format(fps_input_stream))
grabbed, frame = vcap.read() # reading single frame for initialization/ hardware warm-up# processing frames in input stream
num_frames_processed = 0 
start = time.time()
while True :grabbed, frame = vcap.read()if grabbed is False :print('[Exiting] No more frames to read')break# adding a delay for simulating time taken for processing a frame delay = 0.03 # delay value in seconds. so, delay=1 is equivalent to 1 second time.sleep(delay) num_frames_processed += 1cv2.imshow('frame' , frame)key = cv2.waitKey(1)if key == ord('q'):break
end = time.time()# printing time elapsed and fps 
elapsed = end-start
fps = num_frames_processed/elapsed 
print("FPS: {} , Elapsed Time: {} , Frames Processed: {}".format(fps, elapsed, num_frames_processed))# releasing input stream , closing all windows 
vcap.release()
cv2.destroyAllWindows()

多线程代码:

# importing required libraries
import cv2
import time
from threading import Thread  # library for implementing multi-threaded processing# defining a helper class for implementing multi-threaded processing
class WebcamStream:def __init__(self, stream_id=0):self.stream_id = stream_id  # default is 0 for primary camera# opening video capture streamself.vcap = cv2.VideoCapture(self.stream_id)if self.vcap.isOpened() is False:print("[Exiting]: Error accessing webcam stream.")exit(0)fps_input_stream = int(self.vcap.get(5))print("FPS of webcam hardware/input stream: {}".format(fps_input_stream))# reading a single frame from vcap stream for initializingself.grabbed, self.frame = self.vcap.read()if self.grabbed is False:print('[Exiting] No more frames to read')exit(0)# self.stopped is set to False when frames are being read from self.vcap streamself.stopped = True# reference to the thread for reading next available frame from input streamself.t = Thread(target=self.update, args=())self.t.daemon = True  # daemon threads keep running in the background while the program is executing# method for starting the thread for grabbing next available frame in input streamdef start(self):self.stopped = Falseself.t.start()# method for reading next framedef update(self):while True:if self.stopped is True:breakself.grabbed, self.frame = self.vcap.read()if self.grabbed is False:print('[Exiting] No more frames to read')self.stopped = Truebreakself.vcap.release()# method for returning latest read framedef read(self):return self.frame# method called to stop reading framesdef stop(self):self.stopped = True# initializing and starting multi-threaded webcam capture input stream
webcam_stream = WebcamStream(stream_id=0)  # stream_id = 0 is for primary camera
webcam_stream.start()
frame = []
# processing frames in input stream
num_frames_processed = 0
start = time.time()
while True:if webcam_stream.stopped is True:breakelse:frame = webcam_stream.read()# adding a delay for simulating time taken for processing a framedelay = 0.03  # delay value in seconds. so, delay=1 is equivalent to 1 secondtime.sleep(delay)num_frames_processed += 1  # count the number of framescv2.imshow('frame', frame)key = cv2.waitKey(1)if key == ord('q'):breakend = time.time()
webcam_stream.stop()  # stop the webcam stream
# printing time elapsed and fps
elapsed = end - start
fps = num_frames_processed / elapsed
print("FPS: {} , Elapsed Time: {} , Frames Processed: {}".format(fps, elapsed, num_frames_processed))
# closing all windows
cv2.destroyAllWindows()

同理,对于Flask等架构实现rtsp/rtmp推流拉流时也可以使用多线程完成:拉流线程(读取帧)处理线程(跑算法)推流线程(发送帧)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/2812.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

06-C++ 基本算法 - 二分法

&#x1f4d6; 前言 在这个笔记中&#xff0c;我们将介绍二分法这种基本的算法思想&#xff0c;以及它在 C 中的应用。我们将从一个小游戏猜数字开始&#xff0c;通过这个案例来引出二分法的概念。然后我们将详细讲解什么是二分法以及它的套路和应用。最后&#xff0c;我们还会…

在 3ds Max 中创建逼真的玻璃材质

推荐&#xff1a; NSDT场景编辑器助你快速搭建可二次开发的3D应用场景 尽管本教程基于 3ds Max&#xff0c;但相同的设置适用于许多其他 3D 产品。 注意&#xff1a;单击每个步骤中的缩略图可查看更大的屏幕截图&#xff0c;其中包括视口和用户界面的相关部分。 步骤 1由于本教…

广西学子复读15年,不服从分配。网友:完全是浪费时间

广西学子复读15年&#xff0c;不服从分配。网友&#xff1a;完全是浪费时间 唐尚珺的复读行为引起了网友们的不同解读。有人认为他是一个执念深重的人&#xff0c;目标是考上清华北大&#xff0c;但这个说法是否真实&#xff0c;我们无法确定。无论如何&#xff0c;我们必须认识…

electron+vue3全家桶+vite项目搭建【24】设置应用图标,打包文件的图标

文章目录 引入实现步骤测试结果 引入 demo项目地址 在electron中&#xff0c;我们可以通过electron-builder的配置文件来设置打包后的应用图标 实现步骤 因为mac环境下的图标需要特殊格式&#xff0c;这里我们可以利用electron-icon-builder进行配置 1.引入相关依赖 # 安…

GPT 如此强大,我们可以利用它实现什么?

GPT&#xff08;Generative Pre-trained Transformer&#xff09;是一种基于Transformer结构的预训练语言生成模型&#xff0c;由OpenAI研发。它可以生成高质量的自然语言文本&#xff0c;取得了很好的效果&#xff0c;被广泛应用于各个领域。以下是一些利用GPT实现的应用。 一…

ts中setState的类型

两种方法: 例子: 父组件 const [value, setValue] useState(); <ChildsetValue{setValue} />子组件 interface Ipros {setValue: (value: string) > void } const Child: React.FC<Ipros> (props) > {}

SpringMvc配置静态资源访问路径

文章目录 1. 整体流程2. registry.addResourceHandler()2.1 函数分析2.2 结果演示 3. ResourceHandlerRegistration.addResourceLocations()3.1 函数分析3.2 结果演示 1. 整体流程 1. 写一个配置类继承WebMvcConfigurationSupport 2. 利用 registry.addResourceHandler("…

Vue成绩案例实现添加、删除、显示无数据、添加日期、总分均分以及数据本地化等功能

一、成绩案例 ✅✅✅通过本次案例实现添加、删除、显示无数据、添加日期、总分均分以及数据本地化等功能。 准备成绩案例模板&#xff0c;我们需要在这些模板上面进行功能操作。 <template><div class"score-case"><div class"table">…

nginx基础3——配置文件详解(实用功能篇)

文章目录 一、平滑升级二、修饰符2.1 无修饰符效果2.2 精准匹配&#xff08;&#xff09;2.3 区分大小写匹配&#xff08;~&#xff09;2.4 不区分大小写匹配&#xff08;~*&#xff09;2.5 匹配优先级 三、访问控制四、用户认证五、配置https六、开启状态界面七、rewrite重写u…

matplotlib 3D

import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import numpy as np# 创建一个三维坐标轴 fig plt.figure() ax fig.add_subplot(221, projection3d) xx fig.add_subplot(222) yy fig.add_subplot(223) xy fig.add_subplot(224)# 生成示例数据…

关于你欠缺的NoSQL中的redis和mongoDB

文章目录 前言一、在string list hash结构中&#xff0c;每个至少完成5个命令&#xff0c;包含插入 修改 删除 查询&#xff0c;list 和hash还需要增加遍历的操作命令1、STRING类型2、List类型数据的命令操作&#xff1a;3、举例说明list和hash的应用场景&#xff0c;每个至少一…

echarts图例对齐

富文本不生效&#xff0c;是没有设置lineHeight

企业内部FAQ系统的搭建重要性是什么?

企业内部FAQ系统&#xff08;Frequently Asked Questions&#xff0c;常见问题解答系统&#xff09;的搭建对于企业来说具有重要的意义。它可以帮助企业有效地管理和解决员工和客户的常见问题&#xff0c;提高工作效率和服务质量。 企业内部FAQ系统搭建的重要性&#xff1a; …

Python批量实现Word、EXCLE、PPT转PDF文件

一、绪论背景 在日常办公和文档处理中&#xff0c;有时我们需要将多个Word文档、Excel表格或PPT演示文稿转换为PDF文件。将文档转换为PDF格式的好处是它可以保留文档的布局和格式&#xff0c;并且可以在不同平台上进行方便的查看和共享。 本篇博文将介绍如何使用Python编程语言…

lua脚本语言学习笔记

Lua 是一种轻量小巧的脚本语言&#xff0c;用标准C语言编写并以源代码形式开放&#xff0c; 其设计目的是为了嵌入应用程序中&#xff0c;从而为应用程序提供灵活的扩展和定制功能。 因为我们使用redis的时候一般要写lua脚本&#xff0c;这篇文章就介绍一下lua脚本语言的基础用…

旅行社优惠卡app软件开发

旅游行业的不断发展&#xff0c;越来越多的旅行社开始推出各种优惠卡来吸引游客。而随着智能手机的普及&#xff0c;开发一款旅行社优惠卡APP软件成为了一种必然的趋势。 该软件的主要功能是提供旅行社的各种优惠卡信息&#xff0c;包括优惠卡的种类、价格、使用范围、有效…

Pytorch如何打印与Keras的model.summary()类似的输出

1 Keras的model.summary() 2 Pytorch实现 2.1 安装torchsummary包 pip install torchsummary2.2 代码 import torch import torch.nn as nn import torch.nn.functional as F from torchsummary import summaryclass Net(nn.Module):def __init__(self):super(Net, self).__…

【Spring Boot学习一】创建项目 Spring Boot的配置文件

目录 一、安装插件 二、创建Spring Boot项目 1、创建项目 1.1 使用IDEA创建 1.2 网页版本创建 2、项目目录介绍与运行 三、Sping Boot的配置文件&#xff08;重点&#xff09; &#x1f337;1、.properties配置文件 &#xff08;1&#xff09;基础语法&#xff1a;Key …

我在VScode学Java类与对象(Java显式参数和隐式参数、静态方法+main方法、Java访问修饰符、static关键字、Java的包、对象数组)第三辑

我的个人博客主页&#xff1a;如果’真能转义1️⃣说1️⃣的博客主页 关于Java基本语法学习---->可以参考我的这篇博客&#xff1a;《我在VScode学Java》 续《我在VScode学Java&#xff08;Java的类与对象&#xff09;》 方法会操作对象并访问他们的实例字段。 伍._. 显式参…

elementUI el-radio 无法点击的问题

<el-form-item label"B端客户类型" prop"user_type"><template slot"label"><span>B端客户类型</span><el-tooltip effect"dark" placement"top" content"B端大客户账期有效,只有设置该类型…