python入门基础总结笔记(9)——进程和线程
进程和线程?
- 现代操作系统:比如Mac OS X,UNIX,Linux,Windows等,都是支持“多任务”的操作系统。
- 多任务:操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。
- 单核CPU执行多任务时,操作系统轮流让各个任务交替执行**。 多核CPU上并行执行多任务。
- 什么是进程?
一个任务就是一个进程,比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。 - 什么是线程?
有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
目录
- 多进程
- 多线程
- 进程 vs 线程
- 分布式进程
1.多进程
1.1 multiprocessing模块
要编写多进程的服务程序,Unix/Linux无疑是正确的选择。
如果要在Windows上用Python编写多进程的程序,由于Windows没有fork调用,需要用到Python提供的一个跨平台的多进程支持:multiprocessing模块
multiprocessing模块 提供了一个 Process类 来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:
from multiprocessing import Process
import os
# 子进程要执行的代码
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid())) #获取子进程id
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
#创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例
p = Process(target=run_proc, args=('test',))
print('Child process will start.')
p.start() #启动
p.join() #子程序结束后再往下运行
print('Child process end.')
#结果
Parent process 3220.
Child process will start.
Run child process test (6200)...
Child process end.
注意:其中os.getpid()获取当前进程id , os.getppid()获取父进程id
1.2 Pool(批量创建子进程)
如果要启动大量的子进程,可以用进程池的方式批量创建子进程:
from multiprocessing import Pool
import os, time, random
def long_time_task(name):
print('Run task %s (%s)...' % (name, os.getpid())) #获取当前进程id
start = time.time() #返回当前时间的时间戳
time.sleep(random.random() * 3) #线程推迟指定的时间运行,单位为秒
#random.random()随机生成一个[0,1)之间的随机数
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) #设置最多同时执行4个进程
for i in range(5): #循环0-4
p.apply_async(long_time_task, args=(i,))#创建子进程时,传入一个执行函数和函数的参数
print('Waiting for all subprocesses done...')
p.close()
p.join()
print('All subprocesses done.')
结果:
Parent process 9872.
Waiting for all subprocesses done...
Run task 0 (6212)...
Run task 1 (6240)...
Run task 2 (3336)...
Run task 3 (9020)...
Task 0 runs 0.75 seconds.
Run task 4 (6212)...
Task 3 runs 0.55 seconds.
Task 2 runs 1.75 seconds.
Task 1 runs 2.18 seconds.
Task 4 runs 2.69 seconds.
All subprocesses done.
Pool设置同时最多执行4个进程,task 0,1,2,3是立刻执行的,而task 4要等待前面某个task完成后才执行。也可以同时跑6个进程。
对 Pool 对象调用 join() 方法会等待所有子进程执行完毕,调用 join() 之前必须先调用 close(),调用 close() 之后就不能继续添加新的 Process 了。
1.3 进程间通信
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()
结果:
Process to write: 7152
Put A to queue...
Process to read: 2328
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.
由于 Windows 没有 fork 调用,因此,multiprocessing 需要“模拟”出 fork 的效果。
父进程所有Python对象都必须通过pickle序列化再传到子进程去,所以,如果multiprocessing在Windows下调用失败了,要先考虑是不是pickle失败了。
2.多线程
多任务可以由多进程完成,也可以由一个进程内的多线程完成,一个进程至少有一个线程。
高级语言通常都内置多线程的支持,Python也不例外,Python的标准库提供了threading高级模块
启动一个线程就是把一个函数传入并创建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)
结果:
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.
由于任何进程默认就会启动一个线程,我们把该线程称为主线程。主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。
current_thread()函数:它永远返回当前线程的实例。
2.1 Lock
多线程和多进程最大的不同:
多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响。
多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,
因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
import time, threading
# 假定这是你的银行存款:
balance = 0 #定义变量
def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(2000000):
change_it(n)
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)
#结果
-8
以上启动两个线程,先存后取,理论上结果应该为0,但是,由于线程的调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,balance的结果就不一定是0了。
如果我们要确保balance计算正确,就要给change_it()上一把锁:
balance = 0
lock = threading.Lock()
def run_thread(n):
for i in range(100000):
# 先要获取锁:
lock.acquire()
try:
# 放心地改吧:
change_it(n)
finally:
# 改完了一定要释放锁:
lock.release()
该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。
2.2 多核CPU
因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,因此多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核,无法多线程并行。
Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。
3.进程vs线程
3.1 多进程和多线程优缺点
多进程和多线程是实现多任务最常用的两种方式。两种方式的优缺点如下:
多进程
- 优点:稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了)
- 缺点:创建进程的代价大;另外,在内存和CPU的限制下操作系统能同时运行的进程数也是有限的。
多线程
- 优点: 多线程的效率比多进程要高一些。
- 缺点:任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。
无论是多进程还是多线程,只要数量一多,效率肯定上不去,因为多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
3.2 计算密集型 vs.IO密集型
- 计算密集型
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。
这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
- IO密集型
IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。
对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
3.3 异步IO
考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。
现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型。
对应到Python语言,单线程的异步编程模型称为协程。
4.分布式进程
在Thread(多线程)和Process(多进程)中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
Python的 multiprocessing模块 不但支持多进程,其中managers子模块 还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。