多进程
Unix/Linux操作系统提供了一个fork()
系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()
调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
想要在windows平台编写多进程,需要引入multiprocessing模块。
from multiprocessing import Process
import os
def run_proc(name):
print('child pid %s %s /but parent is %s' % (name, os.getpid(), os.getppid()))
#fork出的子进程通过getppid()函数可以拿到父进程id
if __name__ == '__main__':
print('parent pid is %s' % os.getpid())
p = Process(target=run_proc, args=('test',)) #multiprocessing模块提供了一个Process类来代表一个进程对象,在这里实例化这个类,传入执行函数及函数参数
p.start() #启动子进程实例
p.join() #join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
print('child end')
程序结果:
parent pid is 4736
child pid test 5316 /but parent is 4736
child end
join()函数的意思就是,在子进程和主进程做到同时结束
如果注释掉p.join()这一行代码
输出如下:
parent pid is 8152
child end
child pid test 2808 /but parent is 8152
子进程提前结束了,就导致了不同步。
进程池方式可以大量创建子进程
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() #close()函数是指进程池添加完后不允许新的进程加入
p.join()
print('All subprocesses done.') #Pool(4)代表当前cpu为4核心,所以能同时进行4个子进程 多出的一个task4处于等待
执行结果:
Parent process 5460.
Waiting for all subprocesses done...
Run task 0 (9544)...
Run task 1 (7808)...
Run task 2 (8176)...
Run task 3 (4336)...
Task 3 runs 1.67 seconds.
Run task 4 (4336)...
Task 0 runs 1.95 seconds.
Task 2 runs 1.95 seconds.
Task 4 runs 0.31 seconds.
Task 1 runs 2.52 seconds.
All subprocesses done.
apply_async()异步函数,传入执行函数和函数参数。使每一个子进程不在同一时刻将子进程交给cpu。
进程间的通信
Process
之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing
模块包装了底层的机制,提供了Queue
、Pipes
等多种方式来交换数据。
我们以Queue
为例,在父进程中创建两个子进程,一个往Queue
里写数据,一个从Queue
里读数据:
from multiprocessing import Process, Queue
import os, time, random
#写数据进程执行的代码:
def write(q):
print('Process to write: %s parent pid is : %s' % (os.getppid(), 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 parent pid is : %s ' % (os.getpid(), os.getppid()))
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.start() # 启动子进程pw,写入:
pr.start() # 启动子进程pr,读取:
pw.join() # 等待pw结束:
pr.terminate() # pr进程里是死循环,无法等待其结束,只能强行终止:
执行结果:
Process to write: 6264 parent pid is : 1740
Put A to queue...
Process to read: 5296 parent pid is : 1740
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.
多线程
Python的标准库提供了两个模块:_thread
和threading
,_thread
是低级模块,threading
是高级模块,对_thread
进行了封装。绝大多数情况下,我们只需要使用threading
这个高级模块。
import threading
import time
import os
# 新线程执行的代码:
def loop():
print('thread %s is running...' % threading.current_thread().name) # threading.current_thread()函数可以拿到当前执行线程的名称
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('Main pid is %s' % os.getpid())
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)
执行结果:
Main pid is 8380
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.
线程锁LOCk
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
我的理解是,每个进程都会申请属于自己的内存,而线程存在的进程只会申请一次内存、只要存在于同一个进程内的所有线程变量都是共享的。
这个例子是线程共享变量之后带来的问题
import threading
# 假定这是你的银行存款:
balance = 0
def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(100000):
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)
每次运行输出结果都是不一样的:0,8,--8等等;与预期的balance恒为0不同
原因就是t1和t2线程交替执行,变量共享导致了结果的多变性。
从代码看,t1先于t2执行;从结果看,实际上线程的调度由CPU决定的。
解决方法:
解决线程变量共享的关键是,当线程实例创建并调用函数时,给这个线程调用的函数上锁.t1这个线程使用结束后释放锁,t2再获得锁释放锁.....如此反复下去。
import threading
# 假定这是你的银行存款:
balance = 0
lock = threading.Lock() #创建一个线程锁对象
def change_it(n):
# 先存后取,结果应该为0:
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)
线程锁并不作用于函数上,还是线程上,因为函数实际上还是由线程实例执行。
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
不管你的CPU多少核心,Python并不能真正的做到多线程并发。
Python中由于使用了全局解释锁(GIL)的原因,代码并不能同时在多核上并发的运行,也就是说,Python的多线程不能并发,很多人会发现使用多线程来改进自己的Python代码后,程序的运行效率却下降了。这篇文章对Python中的全局解释锁(GIL)进行了介绍。作者认为这是Python中最令人头疼的问题。
这个文章写得很清晰:最让人头疼的Python问题
听说go语言的并发很强大...但是语法太难qwer...
ThreadLocal
解决共享变量带来的问题另一个方法就是让线程使用自己的局部变量
import threading # 创建全局ThreadLocal对象: local_school = threading.local() def process_student(): # 获取当前线程关联的student: 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()
结果输出:
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请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
计算密集型和IO密集型
计算密集型 vs. IO密集型
是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密集型和IO密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
异步IO
考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。
现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。
对应到Python语言,单线程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。