学习自廖雪峰https://www.liaoxuefeng.com
对于操作系统来说,一个任务就是一个进程(Process),把进程内的这些“子任务”称为线程(Thread)。
一、多进程
1.multiprocessing
模块
Windows上无法用Python编写多进程的程序?multiprocessing
模块就是跨平台版本的多进程模块。
multiprocessing
模块提供了一个Process
类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:
1 from multiprocessing import Process 2 import os 3 4 #子进程要执行的代码 5 #getpid()返回当前进程的标识,getppid()返回父进程的标识 6 def run_proc(name): 7 print('Run child process %s (%s)...' %(name, os.getpid())) 8 9 if __name__ == '__main__': 10 print('Parent process %s.' %os.getpid()) 11 p = Process(target=run_proc, args=('test',)) 12 print('Child proccess will start.') 13 p.start() 14 p.join() 15 print('Child process end.')
OUTPUT:
1 Parent process 15596. 2 Child proccess will start. 3 Run child process test (17620)... 4 Child process end.
创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process
实例,用start()
方法启动,这样创建进程比fork()
还要简单。
join()
方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
2.Pool
如果要启动大量的子进程,可以用进程池的方式批量创建子进程:
from multiprocessing import Pool import os, time, random def long_time_task(name): print('Run task %s (%s)...' % (name, os.getpid())) start = time.time() time.sleep(random.random() *3) end = time.time() print('Task %s runs %0.2f seconds.' % (name, (end - start))) if __name__ == '__main__': print('Parent process %s.' % os.getpid()) p = Pool(4) for i in range(5): p.apply_async(long_time_task, args=(i,)) print('Waiting for all subprocesses done...') p.close() p.join() print('All subprocesses done.')
OUTPUT:
Parent process 14956. Waiting for all subprocesses done... Run task 0 (6208)... Run task 1 (11180)... Run task 2 (17868)... Run task 3 (16036)... Task 2 runs 0.20 seconds. Run task 4 (17868)... Task 3 runs 0.23 seconds. Task 4 runs 0.44 seconds. Task 0 runs 2.37 seconds. Task 1 runs 2.73 seconds. All subprocesses done.
对Pool
对象调用join()
方法会等待所有子进程执行完毕,调用join()
之前必须先调用close()
,调用close()
之后就不能继续添加新的Process
了。
请注意输出的结果,task 0
,1
,2
,3
是立刻执行的,而task 4
要等待前面某个task完成后才执行,这是因为Pool
的默认大小在我的电脑上是4,因此,最多同时执行4个进程。这是Pool
有意设计的限制,并不是操作系统的限制。
3.子进程
很多时候,子进程并不是自身,而是一个外部进程。我们创建了子进程后,还需要控制子进程的输入和输出。
subprocess
模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。
下面的例子演示了如何在Python代码中运行命令nslookup www.python.org
,这和命令行直接运行的效果是一样的:
import subprocess print('$ nslookup www.python.org') r = subprocess.call(['nslookup', 'www.python.org']) print('Exit Code:', r)
OUTPUT:
$ nslookup www.python.org ������: UnKnown Address: 127.0.0.1 Exit Code: 0 *** UnKnown �Ҳ��� www.python.org: No response from server
如果子进程还需要输入,则可以通过communicate()
方法输入:
import subprocess print('$ nslookup') p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, err = p.communicate(b'set q=mx\npython.org\nexit\n') print(output.decode('utf-8')) print('Exit code:', p.returncode)
OUTPUT:
$ nslookup Traceback (most recent call last): File "D:/softwares/PyCharm 2019.3.1/projects/s1/test.py", line 6, in <module> print(output.decode('utf-8')) UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc8 in position 2: invalid continuation byte
可能解码方面出了问题
上面的代码相当于在命令行执行命令nslookup
,然后手动输入:
set q=mx python.org exit
4.进程间的通信
Python的multiprocessing
模块包装了底层的机制,提供了Queue
、Pipes
等多种方式来交换数据。
我们以Queue
为例,在父进程中创建两个子进程,一个往Queue
里写数据,一个从Queue
里读数据:
from multiprocessing import Process, Queue import os, time, random #写数据进程执行的代码 def write(q): print('Process to write: %s' % os.getpid()) for value in ['A','B','C']: print('Put %s to queue...' % value) q.put(value) time.sleep(random.random()) #读数据进程执行的代码 def read(q): print('Process to read: %s' % os.getpid()) while True: value = q.get(True) print('Get %s from queue.' % value) if __name__ == '__main__': #父进程创建Queue,并传给各个子进程 q = Queue() pw = Process(target=write, args=(q,)) pr = Process(target=read, args=(q,)) #启动子进程pw,写入 pw.start() #启动子进程pr,读取 pr.start() #等待pw结束 pw.join() #pr进程里是死循环,无法等待其结束,只能强行终止 pr.terminate()
OUTPUT:
Process to write: 10204 Put A to queue... Process to read: 12392 Get A from queue. Put B to queue... Get B from queue. Put C to queue... Get C from queue.
在Unix/Linux下,multiprocessing
模块封装了fork()
调用,使我们不需要关注fork()
的细节。
由于Windows没有fork
调用,因此,multiprocessing
需要“模拟”出fork
的效果,父进程所有Python对象都必须通过pickle序列化再传到子进程去。
所以,如果multiprocessing
在Windows下调用失败了,要先考虑是不是pickle失败了。
二、多线程
多任务可以由多进程完成,也可以由一个进程内的多线程完成。
我们前面提到了进程是由若干线程组成的,一个进程至少有一个线程。
由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。
Python的标准库提供了两个模块:_thread
和threading
,_thread
是低级模块,threading
是高级模块,对_thread
进行了封装。绝大多数情况下,我们只需要使用threading
这个高级模块。
1.thread
启动一个线程就是把一个函数传入并创建Thread
实例,然后调用start()
开始执行:
import time, threading #新线程执行的代码 def loop(): print('thread %s is running...' % threading.current_thread().name) n = 0 while n < 5: n = n + 1 print('thread %s >>> %s' % (threading.current_thread().name, n)) time.sleep(1) print('thread %s ended.' % threading.current_thread().name) print('thread %s is running...' % threading.current_thread().name) t = threading.Thread(target=loop, name='LoopThread') t.start() t.join() print('thread %s ended.' %threading.current_thread().name)
OUTPUT:
thread MainThread is running... thread LoopThread is running... thread LoopThread >>> 1 thread LoopThread >>> 2 thread LoopThread >>> 3 thread LoopThread >>> 4 thread LoopThread >>> 5 thread LoopThread ended. thread MainThread ended.
由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading
模块有个current_thread()
函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread
,子线程的名字在创建时指定,我们用LoopThread
命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1
,Thread-2
……
2.Lock
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,
所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
import time, threading #假定这是你的银行存款(某种程度上很真实QAQ。。。) balance = 0 lock = threading.Lock() def change_it(n): global balance balance = balance + n balance = balance - n def run_thread(n): for i in range(100000): #先要获取锁 lock.acquire() try: #放心地改吧 change_it(n) finally: #改完一定要释放锁 lock.release() t1 = threading.Thread(target=run_thread, args=(5,)) t2 = threading.Thread(target=run_thread, args=(8,)) t1.start() t2.start() t1.join() t2.join() print(balance)
--------------------------------------------------
output:
0
当多个线程同时执行lock.acquire()
时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。
获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally
来确保锁一定会被释放。
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
小结:
多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。
Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦
3.ThreadLocal
一个ThreadLocal
变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal
解决了参数在一个线程中各个函数之间互相传递的问题。
import threading #创建全局ThreadLocal对象 local_school = threading.local() def process_student(): #获取当前线程相关的sudent std = local_school.student print('Hello, %s (in %s)' % (std, threading.current_thread().name)) def process_thread(name): #绑定ThreadLocal的student local_school.student = name process_student() t1 = threading.Thread(target=process_thread, args=('Alice',), name='Thread-A') t2 = threading.Thread(target=process_thread, args=('Bob',), name='Thread-B') t1.start() t2.start() t1.join() t2.join()
OUTPUT:
Hello, Alice (in Thread-A) Hello, Bob (in Thread-B)
全局变量local_school
就是一个ThreadLocal
对象,每个Thread
对它都可以读写student
属性,但互不影响。你可以把local_school
看成全局变量,但每个属性如local_school.student
都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal
内部会处理。
可以理解为全局变量local_school
是一个dict
,不但可以用local_school.student
,还可以绑定其他变量,如local_school.teacher
等等。
ThreadLocal
最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。