Python开发【第九篇】:进程、线程
什么是进程(process)?
程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于,程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。
什么是线程(thread)?
线程是操作系统能够进行运算调度的最小单位。它被包含在进程中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
进程与线程的区别?
线程共享内存空间,进程的内存是独立的。
同一个进程的线程之间可以直接交流,但两个进程相互通信必须通过一个中间代理。
创建一个新的线程很简单,创建一个新的进程需要对其父进程进行一次克隆。
一个线程可以控制和操作同一进程里的其他线程,但是进程只能操作子进程。
Python GIL(Global Interpreter Lock)
无论开启多少个线程,有多少个CPU,python在执行的时候在同一时刻只允许一个线程允许。
Python threading模块
直接调用
- import threading,time
- def run_num(num):
- """
- 定义线程要运行的函数
- :param num:
- :return:
- """
- print("running on number:%s"%num)
- time.sleep(3)
- if __name__ == '__main__':
- # 生成一个线程实例t1
- t1 = threading.Thread(target=run_num,args=(1,))
- # 生成一个线程实例t2
- t2 = threading.Thread(target=run_num,args=(2,))
- # 启动线程t1
- t1.start()
- # 启动线程t2
- t2.start()
- # 获取线程名
- print(t1.getName())
- print(t2.getName())
- 输出:
- running on number:1
- running on number:2
- Thread-1
- Thread-2
继承式调用
- import threading,time
- class MyThread(threading.Thread):
- def __init__(self,num):
- threading.Thread.__init__(self)
- self.num = num
- # 定义每个线程要运行的函数,函数名必须是run
- def run(self):
- print("running on number:%s"%self.num)
- time.sleep(3)
- if __name__ == '__main__':
- t1 = MyThread(1)
- t2 = MyThread(2)
- t1.start()
- t2.start()
- 输出:
- running on number:1
- running on number:2
Join and Daemon
Join
Join的作用是阻塞主进程,无法执行join后面的程序。
多线程多join的情况下,依次执行各线程的join方法,前面一个线程执行结束才能执行后面一个线程。
无参数时,则等待该线程结束,才执行后续的程序。
设置参数后,则等待该线程设定的时间后就执行后面的主进程,而不管该线程是否结束。
- import threading,time
- class MyThread(threading.Thread):
- def __init__(self,num):
- threading.Thread.__init__(self)
- self.num = num
- # 定义每个线程要运行的函数,函数名必须是run
- def run(self):
- print("running on number:%s"%self.num)
- time.sleep(3)
- print("thread:%s"%self.num)
- if __name__ == '__main__':
- t1 = MyThread(1)
- t2 = MyThread(2)
- t1.start()
- t1.join()
- t2.start()
- t2.join()
- 输出:
- running on number:1
- thread:1
- running on number:2
- thread:2
设置参数效果如下:
- if __name__ == '__main__':
- t1 = MyThread(1)
- t2 = MyThread(2)
- t1.start()
- t1.join(2)
- t2.start()
- t2.join()
- 输出:
- running on number:1
- running on number:2
- thread:1
- thread:2
Daemon
默认情况下,主线程在退出时会等待所有子线程的结束。如果希望主线程不等待子线程,而是在退出时自动结束所有的子线程,就需要设置子线程为后台线程(daemon)。方法是通过调用线程类的setDaemon()方法。
- import time,threading
- def run(n):
- print("%s".center(20,"*")%n)
- time.sleep(2)
- print("done".center(20,"*"))
- def main():
- for i in range(5):
- t = threading.Thread(target=run,args=(i,))
- t.start()
- t.join(1)
- print("starting thread",t.getName())
- m = threading.Thread(target=main,args=())
- # 将main线程设置位Daemon线程,它作为程序主线程的守护线程,当主线程退出时,m线程也会退出,由m启动的其它子线程会同时退出,不管是否执行完成
- m.setDaemon(True)
- m.start()
- m.join(3)
- print("main thread done".center(20,"*"))
- 输出:
- *********0*********
- starting thread Thread-2
- *********1*********
- ********done********
- starting thread Thread-3
- *********2*********
- **main thread done**
线程锁(互斥锁Mutex)
一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据就需要线程锁。
- import time,threading
- def addNum():
- # 在每个线程中都获取这个全局变量
- global num
- print("--get num:",num)
- time.sleep(1)
- # 对此公共变量进行-1操作
- num -= 1
- # 设置一个共享变量
- num = 100
- thread_list = []
- for i in range(100):
- t = threading.Thread(target=addNum)
- t.start()
- thread_list.append(t)
- # 等待所有线程执行完毕
- for t in thread_list:
- t.join()
- print("final num:",num)
加锁版本
Lock时阻塞其他线程对共享资源的访问,且同一线程只能acquire一次,如多于一次就出现了死锁,程序无法继续执行。
- import time,threading
- def addNum():
- # 在每个线程中都获取这个全局变量
- global num
- print("--get num:",num)
- time.sleep(1)
- # 修改数据前加锁
- lock.acquire()
- # 对此公共变量进行-1操作
- num -= 1
- # 修改后释放
- lock.release()
- # 设置一个共享变量
- num = 100
- thread_list = []
- # 生成全局锁
- lock = threading.Lock()
- for i in range(100):
- t = threading.Thread(target=addNum)
- t.start()
- thread_list.append(t)
- # 等待所有线程执行完毕
- for t in thread_list:
- t.join()
- print("final num:",num)
GIL VS Lock
GIL保证同一时间只能有一个线程来执行。lock是用户级的lock,与GIL没有关系。
RLock(递归锁)
Rlock允许在同一线程中被多次acquire,线程对共享资源的释放需要把所有锁都release。即n次acquire,需要n次release。
- def run1():
- print("grab the first part data")
- lock.acquire()
- global num
- num += 1
- lock.release()
- return num
- def run2():
- print("grab the second part data")
- lock.acquire()
- global num2
- num2 += 1
- lock.release()
- return num2
- def run3():
- lock.acquire()
- res = run1()
- print("between run1 and run2".center(50,"*"))
- res2 = run2()
- lock.release()
- print(res,res2)
- if __name__ == '__main__':
- num,num2 = 0,0
- lock = threading.RLock()
- for i in range(10):
- t = threading.Thread(target=run3)
- t.start()
- while threading.active_count() != 1:
- print(threading.active_count())
- else:
- print("all threads done".center(50,"*"))
- print(num,num2)
这两种锁的主要区别是,RLock允许在同一线程中被多次acquire。而Lock却不允许这种情况。注意,如果使用RLock,那么acquire和release必须成对出现,即调用了n次acquire,必须调用n次的release才能真正释放所占用的锁。
Semaphore(信号量)
互斥锁同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据,比如售票处有3个窗口,那最多只允许3个人同时买票,后面的人只能等前面任意窗口的人离开才能买票。
- import threading,time
- def run(n):
- semaphore.acquire()
- time.sleep(1)
- print("run the thread:%s"%n)
- semaphore.release()
- if __name__ == '__main__':
- # 最多允许5个线程同时运行
- semaphore = threading.BoundedSemaphore(5)
- for i in range(20):
- t = threading.Thread(target=run,args=(i,))
- t.start()
- while threading.active_count() != 1:
- # print(threading.active_count())
- pass
- else:
- print("all threads done".center(50,"*"))
Timer(定时器)
Timer隔一定时间调用一个函数,如果想实现每隔一段时间就调用一个函数,就要在Timer调用的函数中,再次设置Timer。Timer是Thread的一个派生类。
- import threading
- def hello():
- print("hello,world!")
- # delay 5秒之后执行hello函数
- t = threading.Timer(5,hello)
- t.start()
Event
Python提供了Event对象用于线程间通信,它是有线程设置的信号标志,如果信号标志位为假,则线程等待指导信号被其他线程设置为真。Event对象实现了简单的线程通信机制,它提供了设置信号、清除信号、等待等用于实现线程间的通信。
设置信号
使用Event的set()方法可以设置Event对象内部的信号标志为真。Event对象提供了isSet()方法来判断其内部信号标志的转态,当使用event对象的set()方法后,isSet()方法返回真。
清除信号
使用Event的clear()方法可以清除Event对象内部的信号标志,即将其设为假,当使用Event的clear()方法后,isSet()方法返回假。
等待
Event的wait()方法只有在内部信号为真的时候才会很快的执行并完成返回。当Event对象的内部信号标志为假时,则wait()方法一直等待其为真时才返回。
通过Event来实现两个或多个线程间的交互,下面以红绿灯为例,即启动一个线程做交通指挥灯,生成几个线程做车辆,车辆行驶按红停绿行的规则。
- import threading,time,random
- def light():
- if not event.isSet():
- event.set()
- count = 0
- while True:
- if count < 5:
- print("\033[42;1m--green light on--\033[0m".center(50,"*"))
- elif count < 8:
- print("\033[43;1m--yellow light on--\033[0m".center(50,"*"))
- elif count < 13:
- if
event.isSet(): - event.clear()
- print("\033[41;1m--red light on--\033[0m".center(50,"*"))
- else:
- count = 0
- event.set()
- time.sleep(1)
- count += 1
- def car(n):
- while 1:
- time.sleep(random.randrange(10))
- if
event.isSet(): - print("car %s is running..."%n)
- else:
- print("car %s is waiting for the red light..."%n)
- if __name__ == "__main__":
- event = threading.Event()
- Light = threading.Thread(target=light,)
- Light.start()
- for i in range(3):
- t = threading.Thread(target=car,args=(i,))
- t.start()
queue队列
Python中队列是线程间最常用的交换数据的形式。Queue模块是提供队列操作的模块。
创建一个队列对象
- import queue
- q = queue.Queue(maxsize = 10)
queue.Queue类是一个队列的同步实现。队列长度可以无限或者有限。可以通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1表示队列长度无限。
将一个值放入队列中
- q.put("a")
调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元。如果block为0,put()方法将引发Full异常。
将一个值从队列中取出
- q.get()
调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且block为True,get()就使调用线程暂停,直到有项目可用。如果队列为空且block为False,队列将引发Empty异常。
Python Queue模块有三种队列及构造函数
- # 先进先出
- class queue.Queue(maxsize=0)
- # 先进后出
- class queue.LifoQueue(maxsize=0)
- # 优先级队列级别越低越先出
- class queue.PriorityQueue(maxsize=0)
常用方法
- q = queue.Queue()
- # 返回队列的大小
- q.qsize()
- # 如果队列为空,返回True,反之False
- q.empty()
- # 如果队列满了,返回True,反之False
- q.full()
- # 获取队列,timeout等待时间
- q.get([block[,timeout]])
- # 相当于q.get(False)
- q.get_nowait()
- # 等到队列为空再执行别的操作
- q.join()
生产者消费者模型
在开发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不再等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
最基本的生产者消费者模型的例子。
- import queue,threading,time
- q = queue.Queue(maxsize=10)
- def Producer():
- count = 1
- while True:
- q.put("骨头%s"%count)
- print("生产了骨头",count)
- count += 1
- def Consumer(name):
- while q.qsize() > 0:
- print("[%s] 取到[%s]并且吃了它..."%(name,q.get()))
- time.sleep(1)
- p = threading.Thread(target=Producer,)
- c1 = threading.Thread(target=Consumer,args=("旺财",))
- c2 = threading.Thread(target=Consumer,args=("来福",))
- p.start()
- c1.start()
- c2.start()
多线程multiprocessing
multiprocessing包是Python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。该进程可以运行在Python程序内部编写的函数。该Process对象与Thread对象的用法相同,也有start()、run()、join()的方法。此外multiprocessing包中也有Lock、Event、Semaphore、Condition类(这些对象可以像多线程那样,通过参数传递各个进程),用以同步进程,其用法与threading包中的同名类一致。所以,multiprocessing的很大一部分与threading使用同一套API,只不过换到了多进程的情景。
注意:
在UNIX平台上,当某个进程结束以后,该进程需要被父进程调用wait,否则进程成为僵尸进程(Zombie)。所以,有必要对每个Process对象调用join()方法(实际上等同于wait)。对于多线程来说,由于只有一个进程,所以不存在此必要性。
multiprocessing提供了threading包中没有的IPC(比如Pipe和Queue),效率上更高。应优先考虑Pipe和Queue,避免使用Lock、Event、Semaphore、Condition等同步方式(因为它们占据的不是用户进程的资源)。
多进程应该避免共享资源。在多线程中,我们可以比较容易地共享资源,比如使用全局变量或传递参数。在多进程情况下,由于每个进程有自己独立的内存空间,以上方法并不合适。此时我们可以通过共享内存和Manager的方法来共享资源。但这样做提高了程序的复杂度,并因为同步的需要而降低了程序的效率。
Process.PID中保存有PID,如果进程还没有start(),则PID为None。
下面各个线程和进程都做同一件事——打印PID。但是,所有的任务在打印的时候都会向同一个标准输出(stdout)输出,这样输出的字符会混合在一起无法阅读。使用Lock同步,在一个任务输出完成之后,在允许另一个任务输出,可以避免多个任务同时向终端输出。
- import os,threading,multiprocessing
- def info(sign,lock):
- lock.acquire()
- print(sign,os.getpid())
- lock.release()
- print("Main:",os.getpid())
- if __name__ == "__main__":
- record = []
- lock = threading.Lock()
- for i in range(5):
- thread = threading.Thread(target=info,args=("thread",lock))
- thread.start()
- record.append(thread)
- for i in record:
- i.join()
- print("---------------")
- record2 = []
- lock2 = multiprocessing.Lock()
- for i in range(5):
- process = multiprocessing.Process(target=info,args=("process",lock2))
- process.start()
- record.append(process)
- for i in record2:
- i.join()
所有的Thread的PID都与主程序相同,而每个Process都有一个不同的PID。
进程间通讯
不同进程间内存是不共享的,要想实现两个进程间的数据交换,可以使用Queue和 Pipe。
Pipe
Pipe可以是单向(half-duplex),也可以是双向(duplex)。通过mutiprocessing.Pipe(duplex=False)创建单向管道(默认为双向)。一个进程从pipe一端输入对象,然后被pipe另一端的进程接收,单向管道只允许管道一端的进程输入,而双向管道则允许从两端输入。
- from multiprocessing import Process,Pipe
- def f(conn):
- conn.send([42,None,"Hello"])
- conn.close()
- if __name__ == "__main__":
- parent_conn,child_conn = Pipe()
- p = Process(target=f,args=(child_conn,))
- p.start()
- print(parent_conn.recv())
- p.join()
Pipe对象建立的时候,返回一个含有两个元素的表,每个元素代表Pipe的一端(Connection对象)。对Pipe的某一端调用send()方法来传送对象,在另一端使用recv()来接收。
Queue
使用方法跟threading里的queue差不多。
Queue与Pipe相类似,都是先进先出的结构。但Queue允许多进程放入,多个进程从队列取出对象。Queue使用multiprocessing.Queue(maxsize)创建,maxsize表示队列中可以存放对象的最大数量。
一些进程使用put()在Queue中放入字符串,这个字符串中包含PID和时间。另一个进程从Queue中取出,并打印自己的PID以及get()的字符串。
- from multiprocessing import Process,Queue
- def f(q):
- q.put([44,None,"Hello"])
- if __name__ == "__main__":
- q = Queue()
- p = Process(target=f,args=(q,))
- p.start()
- print(q.get())
- p.join()
共享变量Managers
Python中进程间共享数据,处理基本的Queue和Pipe外,还提供了更高层次的封装。使用multiprocessing.Managers可以简单地使用这些高级接口。
Manager()返回的manager对象控制了一个server进程,此进程包含的python对象可以被其他的进程通过proxies来访问,从而达到多进程数据通信且安全。
Manager支持的类型有list、dict、Namespace、Lock、RLock、Semaphore、BoundedSemaphore、Condition、Event、Queue、Value和Array。
- from multiprocessing import Manager,Process
- import os
- def f(d,l):
- d[os.getpid()] = os.getpid()
- l.append(os.getpid())
- print(l)
- if __name__ == "__main__":
- with Manager() as manager:
- d = manager.dict()
- l = manager.list(range(5))
- p_list = []
- for i in range(10):
- p = Process(target=f,args=(d,l,))
- p.start()
- p_list.append(p)
- for res in p_list:
- res.join()
- print(d)
- print(l)
进程同步
当多个进程需要访问共享资源的时候,Lock可以用来避免访问的冲突。
- from multiprocessing import Process,Lock
- def f(l,i):
- l.acquire()
- try:
- print("hello word",i)
- finally:
- l.release()
- if __name__ == "__main__":
- lock = Lock()
- for i in range(10):
- Process(target=f,args=(lock,i)).start()
进程池
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可用的进程为止。
进程池中有两个方法:apply(阻塞)和apply_async(非阻塞)。
apply(func[,args[,kwds]])
apply用于传递不定参数,主进程会阻塞与函数。主进程的执行流程同单进程一致。
apply_async(func[,args[,kwds[,callback]]])
与apply用法一致,但它是非阻塞的且支持结果返回后进行回调。主进程循环运行过程中不等待apply_async的返回结果,在主进程结束后,即使子进程还未返回整个程序也会退出。虽然apply_async是非阻塞的,但其返回结果的get方法却是阻塞的,如使用result.get()会阻塞主进程。如果对返回结果不感兴趣,那么可以在主进程中使用pool.close与pool.join来防止主进程退出。注意join方法一定要close或terminate之后调用。
- from multiprocessing import Process,Pool,freeze_support
- import time,os
- def Foo(i):
- time.sleep(2)
- print("in process",os.getpid())
- return i + 100
- def Bar(arg):
- print("-->exec done:",arg,os.getpid())
- if __name__ == "__main__":
- # freeze_support()
- # 允许进程池同时放入3个进程
- pool = Pool(processes=3)
- print("主进程",os.getpid())
- for i in range(10):
- # callback回调函数,每个进程结束的时候调用
- pool.apply_async(func=Foo,args=(i,),callback=Bar)
- # 串行
- # pool.apply(func=Foo,args=(i,))
- print('end')
- pool.close()
- # 进程池中进程执行完毕后再关闭,如果注释程序直接关闭
- pool.join()
close()
关闭pool,使其不再接受新的任务。
terminate()
结束工作进程,不再处理未处理的任务。
join()
主进程阻塞等待子进程的退出,join方法要在close或terminate之后使用。
Python开发【第九篇】:进程、线程的更多相关文章
- 《python开发技术详解》|百度网盘免费下载|Python开发入门篇
<python开发技术详解>|百度网盘免费下载|Python开发入门篇 提取码:2sby 内容简介 Python是目前最流行的动态脚本语言之一.本书共27章,由浅入深.全面系统地介绍了利 ...
- iOS开发多线程篇—创建线程
iOS开发多线程篇—创建线程 一.创建和启动线程简单说明 一个NSThread对象就代表一条线程 创建.启动线程 (1) NSThread *thread = [[NSThread alloc] in ...
- 李洪强iOS开发Swift篇---12_NSThread线程相关简单说明
李洪强iOS开发Swift篇---12_NSThread线程相关简单说明 一 说明 1)关于多线程部分的理论知识和OC实现,在之前的博文中已经写明,所以这里不再说明. 2)该文仅仅简单讲解NSThre ...
- 【python自动化第九篇:进程,线程,协程】
简要: paramiko模块 进程与线程 python GIL全局解释器锁 一.PARAMIKO模块 实现远程ssh执行命令 #!/usr/bin/env python # -*- coding:ut ...
- python基础-第九篇-9.1初了解Python线程、进程、协程
了解相关概念之前,我们先来看一张图 进程: 优点:同时利用多个cpu,能够同时进行多个操作 缺点:耗费资源(重新开辟内存空间) 线程: 优点:共享内存,IO操作时候,创造并发操作 缺点:抢占资源 通过 ...
- iOS开发多线程篇 04 —线程间的通信
iOS开发多线程篇—线程间的通信 一.简单说明 线程间通信:在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信 线程间通信的体现 1个线程传递数据给另1个线程 在1个线程中执行完特定任 ...
- python开发第一篇:初识python
一. Python介绍 python的创始人为吉多·范罗苏姆(Guido van Rossum).1989年的圣诞节期间,吉多·范罗苏姆为了在阿姆斯特丹打发时间,决心开发一个新的脚本解释程序,作为AB ...
- iOS开发多线程篇 03 —线程安全
iOS开发多线程篇—线程安全 一.多线程的安全隐患 资源共享 1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源 比如多个线程访问同一个对象.同一个变量.同一个文件 当多个线程访问同一块 ...
- Python之旅Day9 进程&线程
进程 线程 多进程 多线程
- python基础-第九篇-9.3线程池
简单版 import queue import threading class ThreadPool(object): def __init__(self, max_num=20): self.que ...
随机推荐
- 软件测试:2.Two Faulty Programs
软件测试:2.Two Faulty Programs Questions: 1.Identify the fault; 2.If possible, identify a test case that ...
- uva_answers
uva202: https://blog.csdn.net/lecholin/article/details/70163148 uva1589: https://blog.csdn.net/qq_42 ...
- DataStrom框架深造
根据前一版DataStrom的使用,继续进行了改造和升级;前一版框架只是对服务按照名称注册和调用固化接口 最近研究后台框架,接触了ZBUS框架,我很喜欢ZBUS的前一版,该作者继续升级,已经在向AMQ ...
- [java,2019-01-15] word转pdf
word转pdf jar包 <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j& ...
- Z 字形变换
将一个给定字符串根据给定的行数,以从上往下.从左到右进行 Z 字形排列. 比如输入字符串为 "LEETCODEISHIRING" 行数为 3 时,排列如下: L C I R E T ...
- 用Python制作中国地图、地球平面图及球形图
绘制地图在python中主要用到的 basemap 库,这个库是 matplotlib 库中一个用于在 Python 中绘制地图上的 2D 数据的工具包. 首先安装库: 1.安装 geos 库:Pyt ...
- iOS开发- 相机(摄像头)获取到的图片自动旋转90度解决办法
http://blog.csdn.net/hitwhylz/article/details/39518463
- Zabbix 配置监控主机
1.新建主机: zabbix中的主机(Host)是要监控的网络实体(物理的,或者虚拟的);zabbix中,对于主机的定义非常灵活,它可以时一台物理服务器,一个网络交换机,一个虚拟机或者一些应用 zab ...
- 打造适合自己的vim编辑器方法总结
vim使用方法总结 说明:这是打造适合自己的vim编辑器的进阶方法,关于vim基础知识,请自行百度.也可参考文章末尾推荐blog网址 如果觉得自己打造vim编辑器麻烦,可以从github上面克隆一个, ...
- 6. spring启动类配置问题
1. @SpringBootApplication(scanBasePackages={"com.example.*"}) 相当与 @SpringBootApplication @ ...