大师兄的Python学习笔记(十): 多进程和多线程
大师兄的Python学习笔记(九): logging模块与日志
大师兄的Python学习笔记(十一): 时间模块time,datetime和calendar
一、关于多进程和多线程
- 多进程和多线程是让系统同时运行多个任务的做法。
- 使用多进程/多线程可以充分利用闲置的系统资源。
- 使用多进程/多线程可以避免由于阻塞造成的程序停滞(尤其是网络请求)。
- 无论进程或线程,都不是越多越好,就像在厨房做饭也不是人越多越快一个道理。
1. 进程和线程
1.1进程
- 进程是程序运行的过程。
- 是操作系统进行资源分配和调度的一个独立单位。
- 是应用程序运行的载体。
- 进程包含地址空间,内存,数据栈等。
- 比如在系统中运行一个程序,就是一个进程。
- 一个进程是由一个或多个线程组成的。
1.2 线程
- 线程是程序执行中的执行片段。
- 是程序执行流的最小单元。
- 是处理器调度和分派的基本单位。
- 一个进程至少包含一个线程,也可以包含多个线程。
1.3 多进程
- 每个进程都有独立的运行环境。
- 由于进程的独立性高,所以多进程相对稳定。
- 相应的,多进程消耗大量系统资源。
- 多个进程间的数据传输很麻烦。
1.4 多线程
- 一个进程中的多个线程可以共享数据和上下文运行环境。
- 多线程消耗的资源相对少,速度也快一些。
- 多线程的稳定性差,可能某个子线程崩溃,整个进程会被迫停止。
- 多线程间的信息传输相对容易。
- 现在主流使用多线程,多进程使用的少。
1.5 全局解释器锁(GIL)
- GIL存在于CPython,也就是我们常用的Python版本。
- python代码的执行由Python虚拟机控制, 在任意时刻只有一个线程在解释器中运行。
- 具体的情况大概是GIL控制虚拟机的访问控制,在同一时刻只给一个线程发锁。
- 这样的后果是导致CPython不能利用物理多核的性能加速运行。
- 随着版本更新,未来有可能解决GIL的问题。
- 目前也有一些变通的办法,比如使用线程池,ctype等。
二、Python中的多进程
- 主要有os.fork,multiprocessing和subprocess等方法。
1. os.fork
1)关于os.fork
- 可以在UNIX/LINUX系统下使用。
- 用于创建多进程。
2)os.fork的使用
- 在调用os.fork时,会返回两次,分别是当前进程(父进程)和当前进程的复制(子进程)。
- 用os.getpid()可以获得当前进程的进程id。
- 用os.getppid()可以获得当前进程父进程进程的进程id。
# fork1.py >>>import os >>>def child(): >>> print('this is child:',os.getpid()) >>> print('my parent is:',os.getppid()) >>>def parent(): >>> newpid = os.fork() >>> if newpid ==0: >>> child() >>> else: >>> print('this is parent:',os.getpid(),newpid) >>>parent() # linux控制台 >>>python fork1.py this is parent: 21665 21666 this is child: 21666 my parent is: 21665
2. multiprocessing包
1)关于multiprocessing
- 用于创建多线程。
- multiprocessing在python3中是标准库内容。
- 可以在Windows系统、Unix/Linux系统跨平台使用。
- 接口和线程相似。
2)multiprocessing.Process方法
- 用Process类来创建进程对象。
- 每个对象都是一个新的进程。
# test_Process.py >>>from multiprocessing import Process >>>import time >>>def Dog(): >>> for i in range(1,5): >>> print('汪汪!') >>> time.sleep(1) >>>def Cat(): >>> for i in range(1,5): >>> print('喵喵~') >>> time.sleep(1) >>>p1 = Process(target=Dog) # 创建进程1,调用Dog函数 >>>p2 = Process(target=Cat) # 创建进程2, 调用Cat函数 >>>p1.start() # 开始进程1 >>>p2.start() # 开始进程2 # console >>>ubuntu@VM-0-4-ubuntu:~$ python test_process.py # 这里的运行顺序不是固定的 喵喵~ 汪汪! 喵喵~ 汪汪! 汪汪! 喵喵~ 喵喵~ 汪汪!
3)multiprocessing.Process的run()方法
- 使用了Process类的继承方式。
- 当没有个Process指定target时,会自动运行类的run()方法。
- 这里本质和2)是相同的
# test_process2.py >>>from multiprocessing import Process >>>import time >>>class Process_Dog(Process): >>> def __init__(self): >>> super().__init__() >>> def run(self): >>> for i in range(5): >>> print('汪汪!') >>> time.sleep(1) >>>class Process_Cat(Process): >>> def __init__(self): >>> super().__init__() >>> def run(self): >>> for i in range(1, 5): >>> print('喵喵~') >>> time.sleep(1) >>>p_dog = Process_Dog() >>>p_cat = Process_Cat() >>>p_dog.start() >>>p_cat.start() # console >>>ubuntu@VM-0-4-ubuntu:~$ python test_process2.py 喵喵~ 汪汪! 汪汪! 喵喵~ 喵喵~ 汪汪! 汪汪! 喵喵~ 汪汪!
4)multiprocessing.Pool进程池
- 在使用大量进程时可以使用进程池Pool。
- 进程池开启的个数默认是CPU的个数。
- 可以使用apply()或apply_async()(阻塞和非阻塞)的方式向线程池中添加Process。
- 可以用join()方法启动线程池中的所有线程,并等待执行完毕。
- 调用join()之前需要先调用close()方法。
- 调用close()之后就不能继续添加新的Process。
#test_pool.py >>>from multiprocessing import Pool >>>import time >>>def Dog(): >>> for i in range(1,5): >>> print('汪汪!') >>> time.sleep(1) >>>def Cat(): >>> for i in range(1,5): >>> print('喵喵~') >>> time.sleep(1) >>>pool = Pool(2) # 定义线程池大小 >>>pool.apply_async(Dog) # 以非阻塞模式加入线程池 >>>pool.apply_async(Cat) >>>pool.close() # 关闭线程池,准备调用 >>>pool.join() # 主进程阻塞,等待子进程退出 # console >>>ubuntu@VM-0-4-ubuntu:~$ python test_pool.py # 这里的结果是固定的 汪汪! 喵喵~ 汪汪! 喵喵~ 汪汪! 喵喵~ 汪汪! 喵喵~
5)多进程和进程池的区别
假设餐馆有20个订单,只有3个锅炒菜:
- 多进程:20个厨子(进程)抢锅,每个厨子只炒1分钟自己的菜,到时间就换人。等于20个进程做20个任务。
- 进程池:3个锅(进程池包含3个进程)放在火眼上,哪个厨子(进程)来就继续炒已经在锅里的菜。等于3个进程做20个任务。
3. subprocess包
1)关于subprocess
- 用于启动一个外部进程。
- subprocess在python3中是标准库内容。
- 可以在Windows系统、Unix/Linux系统跨平台使用。
2)subprocess.call(args,*,stdin=None,stdout=None,stderr=None,shell=False,timeout=None)
- 用于执行shell指令。
- args表示提供的指令列表。
- stdin表示输入方式。
- stdout表示输出方式。
- stderr表示输出错误的方式。
- shell表示是否和shell直接传字符串。
- timeout表示连接超时时间。
#test_subprocess.py >>>print('here in subprocess!') #python console >>> import subprocess >>> subprocess.call(['python','test_subprocess.py']) here in subprocess! 0
3)subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=())
- 用于创建外部进程并处理复杂的交互。
>>>subprocess.Popen(['python','test_subprocess.py'],shell=True) <subprocess.Popen object at 0x7f58c9ba19d0> Python 3.7.5 (default, Oct 25 2019, 15:51:11) [GCC 7.3.0] :: Anaconda, Inc. on linux Type "help", "copyright", "credits" or "license" for more information.
4) subprocess.getoutput(cmd)
- 返回在shell中执行cmd的输出。
>>> subprocess.getoutput('ps') ' PID TTY TIME CMD\n13239 pts/0 00:00:00 bash\n13284 pts/0 00:00:00 python\n13411 pts/0 00:00:00 sh\n13412 pts/0 00:00:00 ps'
5) subprocess.communicate(input=None, timeout=None)
- 实现子进程的输入。
#input.py >>>age = input('please input your age:') #这里会要求在console输入 >>>print('you are {age} years old.'.format(age=age)) #subprocess_input.py >>>import subprocess >>>p = subprocess.Popen(['python','input.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) # 建立进程的同时,将输入输出都绑定到subprocess的管道上 >>>output,err = p.communicate(b'19') # 这里返回input的输出,字节流需要用byte输入。 >>>print(output.decode('utf-8')) # 解码byte流 #console >>>ubuntu@VM-0-4-ubuntu:~$ python subprocess_input.py please input your age:you are 19 years old.
6)subprocess的更多方法。
4. 进程间的通讯
- 由于进程在内存中是独立的,所以通讯机制要比线程复杂很多。
- 主要的通讯方式包含匿名管道、具名管道、套接字和信号四种方式。
4.1 关于管道
- 一个程序在管道一端写入数据,而另一个程序在另一端读取数据。
- 管道有先进先出(FIFO)的特点。
- 管道是单向的,类似共享的内存缓冲。
- 对于两端的程序接口来说,类似简单的文件。
- 管道偏向在操作系统内部使用。
- 管道分为匿名管道和具名管道两种。
4.2 匿名管道
- 匿名管道用
os.pipe()
方法创建。- 匿名管道允许共享文件描述符的线程及进程传递数据。
- 匿名管道仅在进程内部存在。
- 仅可以在Unix平台下使用。
# pipe1.py >>>import os,time >>>def child(pipeout): >>> count=0 >>> while True: >>> time.sleep(count) # 让父进程等待 >>> msg = ('child count {count}'.format(count=count)).encode() # 发送到pipe的内容需要时二进制 >>> os.write(pipeout,msg) # 管道的输入端 >>> count = (count+1)%5 # 最后四位+1 >>>def parent(): >>> pipein,pipeout = os.pipe() # 创建一个管道 >>> if os.fork() == 0: # 创建线程 >>> child(pipeout) # 在子进程中运行child >>> else: >>> while True: # 在父进程中监听管道 >>> line = os.read(pipein,32) >>> print('parent {pid} got {line} at {time}'.format(pid=os.getpid(),line=line,time=time.time())) parent() # console >>>ubuntu@VM-0-4-ubuntu:~$ python pip1.py parent 28186 got b'child count 0' at 1578817490.278132 parent 28186 got b'child count 1' at 1578817491.2795548 parent 28186 got b'child count 2' at 1578817493.2802281 parent 28186 got b'child count 3' at 1578817496.2834265 parent 28186 got b'child count 4' at 1578817500.283922 parent 28186 got b'child count 0' at 1578817500.284008 parent 28186 got b'child count 1' at 1578817501.2851105 parent 28186 got b'child count 2' at 1578817503.2853155 parent 28186 got b'child count 3' at 1578817506.286127 parent 28186 got b'child count 4' at 1578817510.2898583 parent 28186 got b'child count 0' at 1578817510.2899537 parent 28186 got b'child count 1' at 1578817511.2903323 parent 28186 got b'child count 2' at 1578817513.2906644 ... ...
4.3 具名管道
- 具名管道用
os.mkFIFO()
方法创建。- 具名管道与匿名管道的区别是,他与系统中一个真实的文件相关联,而不依赖任何共享的内存。
- 所以它可以长时间存在。
- 对于程序来讲,它是外部文件。
# pipe2.py >>>import os,time,sys >>>fifoname='/tmp/pipefifo' >>>def child(): >>> count = 0 >>> pipeout = os.open(fifoname,os.O_NONBLOCK|os.O_WRONLY) >>> while True: >>> time.sleep(count) >>> msg = ('child count {count}\n'.format(count=count)).encode() # 这里必须有\n >>> os.write(pipeout,msg) # 写入pipe >>> count = (count +1)%5 >>>def parent(): >>> pipein = open(fifoname,'r') # 从pipe读取 >>> while True: >>> line = pipein.readline()[:-1] >>> print('parent {} got {} at {}'.format(os.getpid(),line,time.time())) >>>if __name__ == '__main__': >>> if not os.path.exists(fifoname): >>> os.mkfifo(fifoname) >>> if len(sys.argv) == 1: # 由于本来就是独立的进程,所以不需要再分开进程。 >>> parent() >>> else: >>> child() # console 1 >>>python pipe2.py parent 9847 got b'child count 1' at 1578823592.4824383 parent 9847 got b'child count 2' at 1578823592.4824595 parent 9847 got b'child count 3' at 1578823592.4824812 ... ... # console 2 >>>python pipe2.py -child
4.4 套接字
- 套接字由socket模块实现。
- 可以让数据传输在同一台机器上的不同程序间进行,也可以在远程联网的程序间进行。
- 套接字根据端口号进行识别,而非文件系统的路径名称。
- 可以传输任意Python对象,比如列表、字典和pickle对象等。
- 可以在所有平台上运行。
# socket_test.py >>>from socket import socket,AF_INET,SOCK_STREAM >>>import time >>>port = 10001 >>>host = 'localhost' >>>def server(): >>> sock = socket(AF_INET,SOCK_STREAM) # ip地址 >>> sock.bind(('',port)) # 绑定的端口 >>> sock.listen(5) # 允许5个客户端接入 >>> while True: >>> connection,address = sock.accept() # 开始监听 >>> data = connection.recv(1024) # 获取数据 >>> reply = 'server got: {}'.format(data) # 返回的数据 >>> connection.send(reply.encode()) # 发送返回的数据 >>>def client(name): >>> time.sleep(1) >>> sock = socket(AF_INET,SOCK_STREAM) >>> sock.connect((host,port)) >>> sock.send(name.encode()) >>> reply = sock.recv(1024) >>> sock.close() >>> print('client got: {}'.format(reply)) >>>if __name__ == '__main__': >>> from multiprocessing import Process >>> p_server = Process(target=server) # server 进程 >>> p_server.start() >>> for i in range(5): >>> p_client = Process(target=client,args=('client'+str(i),)) # 5个client进程 >>> p_client.start() >>> p_client.join() #console >>>python socket_test.py client got: b"server got: b'client0'" client got: b"server got: b'client1'" client got: b"server got: b'client2'" client got: b"server got: b'client3'" client got: b"server got: b'client4'"
4.5 信号
- Python中可以使用signal模块发送信号。
- 只能提供一个基于事件的通信机制,不如其它几种方式强大。
- 信号根据编号来识别,而不是堆栈。
- 机制类似跨软件的异常。
- 作用域其实在Python解释器之外,依赖操作系统。
- 在Unix和Windows中都可以使用。
# testSignal.py >>> import signal,time >>> def onSignal(signum,stackframe): >>> print('qq is waiting for dinner',signum,'at',time.asctime()) >>> while True: >>> print('Setting at',time.asctime()) >>> signal.signal(signal.SIGALRM,onSignal) >>> signal.alarm(5) >>> signal.pause() # console >>>python testSignal.py Setting at Mon Jan 13 22:40:54 2020 qq is waiting for dinner 14 at Mon Jan 13 22:40:59 2020 Setting at Mon Jan 13 22:41:00 2020 qq is waiting for dinner 14 at Mon Jan 13 22:41:05 2020 Setting at Mon Jan 13 22:41:00 2020 qq is waiting for dinner 14 at Mon Jan 13 22:41:05 2020 Setting at Mon Jan 13 22:41:00 2020 qq is waiting for dinner 14 at Mon Jan 13 22:41:05 2020
4.6 multiprocessor.queue()方法
- multiprocessing模块封装了底层的通信机制,用起来会更容易。
- multiprocessor.queue()创建了一个先进先出(FIFO)的队列。
- 用put()方法塞入数据,用get()方法获取(pop)数据。
#queue.py >>>from multiprocessing import Process,Queue >>>import os,time queue = Queue() # 创建一个Queue >>>def child(queue): >>> count = 0 >>> while True: >>> time.sleep(count) >>> msg = ('child count {count}'.format(count=count)) >>> queue.put(msg) # 塞入数据 >>> count += 1 >>>def parent(queue): >>> while True: >>> line = queue.get() # 获取数据 >>> print('parent got {} at {}'.format(line, time.time())) >>>if __name__ == '__main__': >>> pp = Process(target=parent,args=(queue,)) # args如果只有一个参数,需要在参数后加",",为了证明他是tuple >>> pc = Process(target=child,args=(queue,)) >>> pp.start() >>> pc.start() >>> pp.join() # 等待pp的结果 # console >>>python queue.py parent got child count 0 at 1578907261.1464262 parent got child count 1 at 1578907262.146753 parent got child count 2 at 1578907264.147026 parent got child count 3 at 1578907267.1476533 parent got child count 4 at 1578907271.1483703
三、Python中的多线程
- Python标准库有两个线程模块,分别是
_thread
和threading
。
1. _thread模块
1)关于_thread模块
- _thread模块比threading要简单。
- 接口层面也相对较低。
- 可以在所有平台使用。
- 这个模块不常用。
2)使用_thread模块
- 使用
_thread.start_new_thread(function,args[,kwargs])
开启新线程。- function - 线程函数。
- args - 传递给线程函数的参数,他必须是个tuple类型。
- kwargs - 可选参数。
>>>import _thread as thread,time >>>def counter(id,count): >>> for i in range(count): >>> time.sleep(1) >>> print('pupply {} is waiting for food,{}st time.'.format(id,i)) >>>for i in range(3): >>> thread.start_new_thread(counter,(i,5)) # 创建并启动3个不同的线程 >>> time.sleep(5) >>> print('food time now!') pupply 0 is waiting for food,0st time. pupply 0 is waiting for food,1st time. pupply 0 is waiting for food,2st time. pupply 0 is waiting for food,3st time. food time now! pupply 0 is waiting for food,4st time. pupply 1 is waiting for food,0st time. pupply 1 is waiting for food,1st time. pupply 1 is waiting for food,2st time. pupply 1 is waiting for food,3st time. food time now! pupply 1 is waiting for food,4st time. pupply 2 is waiting for food,0st time. pupply 2 is waiting for food,1st time. pupply 2 is waiting for food,2st time. pupply 2 is waiting for food,3st time. food time now! pupply 2 is waiting for food,4st time.
2. threading模块
1)关于threading模块
- threading模块封装了_thread模块。
- 接口基于面相和类等较高层面的接口。
- 是多线程的常用模块。
2)使用threading模块
- 使用
threading.thread(target=function_name, args=(function_parameter1, function_parameterN))
开启新线程。- 使用
threading.start()
启动线程。- function对应函数名。
- target对应参数(tuple类型)。
>>>import threading >>>def counter(id,count): >>> for i in range(count): >>> time.sleep(1) >>> print('pupply {} is waiting for food,{}st time.'.format(id,i)) >>>for i in range(3): >>> t = threading.Thread(target=counter,args=(i,5)) # 创建线程 >>> t.start() # 开启线程 >>> time.sleep(5) >>> print('food time now!') pupply 0 is waiting for food,0st time. pupply 0 is waiting for food,1st time. pupply 0 is waiting for food,2st time. pupply 0 is waiting for food,3st time. food time now! pupply 0 is waiting for food,4st time. pupply 1 is waiting for food,0st time. pupply 1 is waiting for food,1st time. pupply 1 is waiting for food,2st time. pupply 1 is waiting for food,3st time. food time now! pupply 1 is waiting for food,4st time. pupply 2 is waiting for food,0st time. pupply 2 is waiting for food,1st time. pupply 2 is waiting for food,2st time. pupply 2 is waiting for food,3st time. food time now! pupply 2 is waiting for food,4st time.
3)使用类的继承方式
- start()方法实际上是把需要并行处理的代码放在run()方法中,并自动调用 run()方法。
- run()方法可以被子类继承和重写。
>>>import threading,time >>>class Mythread(threading.Thread): >>> def __init__(self,id,count,mutex): >>> self.id = id >>> self.count = count >>> self.mutex = mutex # 为了保证输出同步加了线程锁 >>> threading.Thread.__init__(self) >>> def run(self): # 重写run方法 >>> for i in range(self.count): >>> time.sleep(1) >>> with self.mutex: >>> print('pupply {} is waiting for food,{}st time.'.format(self.id,i)) >>>stdoutmutex = threading.Lock() # 创建线程锁 >>>threads = [] >>>for i in range(3): >>> t = Mythread(i,5,stdoutmutex) >>> threads.append(t) >>>for t in threads: >>> t.start() >>>print('food time now!') food time now! pupply 1 is waiting for food,0st time. pupply 0 is waiting for food,0st time. pupply 2 is waiting for food,0st time. pupply 1 is waiting for food,1st time. pupply 0 is waiting for food,1st time. pupply 2 is waiting for food,1st time. pupply 1 is waiting for food,2st time. pupply 0 is waiting for food,2st time. pupply 2 is waiting for food,2st time. pupply 1 is waiting for food,3st time. pupply 0 is waiting for food,3st time. pupply 2 is waiting for food,3st time. pupply 1 is waiting for food,4st time. pupply 0 is waiting for food,4st time. pupply 2 is waiting for food,4st time.
4)关于线程安全
- 当多个线程同时访问一个变量时(共享变量),会产生线程安全问题。
- 需要用锁(Lock)或信号量(Semaphore)来解决线程安全问题。
- 有一些变量类型是不会发生线程安全问题的,比如queue类型。
>>>from threading import Thread >>>import time >>>n = 0 >>>def count(): # 每次计算的结果应该都为0 >>> global n >>> n = n + 1 >>> n = n - 1 >>>def run_thread(): >>> for i in range(10000000): # 大量的计算时产生产生冲突的几率更大 >>> count() >>>t1 = Thread(target=run_thread,args=()) >>>t2 = Thread(target=run_thread,args=()) >>>t1.start() >>>t2.start() >>>t1.join() >>>t2.join() >>>print(n) # 由于线程间的计算冲突,这里算出了错误的结果 12
5)线程锁Lock()
- 线程锁相当于给变量上了一把锁,每次只给一个线程钥匙。
- 用
threading.Lock()
创建锁。- 用
lock.acquire()
交给线程钥匙。- 用
lock.release()
将锁还给系统。>>>from threading import Thread,Lock >>>import time >>>lock = Lock() # 创建锁,由于我用from的方式引用,所以这里可以直接使用函数名。 >>>n = 0 >>>def count(): >>> global n >>> n = n + 1 >>> n = n - 1 >>>def run_thread(): >>> for i in range(10000000): >>> lock.acquire() # 将钥匙交给线程 >>> try: # 如果访问上锁的变量会返回异常,所以这里用需要用到异常处理。 >>> count() >>> finally: >>> lock.release() # 结束后一定要将锁还给系统,否则会造成死锁。 >>>t1 = Thread(target=run_thread,args=()) >>>t2 = Thread(target=run_thread,args=()) >>>t1.start() >>>t2.start() >>>t1.join() >>>t2.join() >>>print(n) # 这次结果就没错了 0
6)信号量Semaphore()
- 信号量Semaphore是锁Lock的进化版本,可以同时给多个线程钥匙。
- 意味着可以允许固定数量的线程访问。
- 用
threading.Semaphore(n)
创建锁,n表示钥匙的数量。- 用
Semaphore.acquire()
交给线程钥匙。- 用
Semaphore.release()
将锁还给系统。>>>from threading import Thread,Semaphore >>>import time >>>sem = Semaphore(3) # 同时允许3个线程访问 >>>n = 0 >>>def count(): >>> global n >>> n = n + 1 >>> n = n - 1 >>>def run_thread(): >>> for i in range(1000000): >>> sem.acquire(5) >>> try: >>> count() >>> finally: >>> sem.release() >>>for i in range(5): >>> t = Thread(target=run_thread,args=()) >>> t.start() >>> t.join() >>>print(n) 0
参考资料
- https://blog.csdn.net/u010138758/article/details/80152151 J-Ombudsman
- https://www.cnblogs.com/zhuluqing/p/8832205.html moisiet
- https://www.runoob.com 菜鸟教程
- http://www.tulingxueyuan.com/ 北京图灵学院
- http://www.imooc.com/article/19184?block_id=tuijian_wz#child_5_1 两点水
- https://blog.csdn.net/weixin_44213550/article/details/91346411 python老菜鸟
- https://realpython.com/python-string-formatting/ Dan Bader
- https://www.liaoxuefeng.com/ 廖雪峰
- https://blog.csdn.net/Gnewocean/article/details/85319590 新海说
- https://www.cnblogs.com/Nicholas0707/p/9021672.html Nicholas
- 《Python学习手册》Mark Lutz
- 《Python编程 从入门到实践》Eric Matthes
本文作者:大师兄(superkmi)